mirror of
https://github.com/iGreed1993/GreedMeter.git
synced 2026-11-10 20:17:33 +00:00
1899 lines
64 KiB
Lua
1899 lines
64 KiB
Lua
--[[
|
|
GreedMeter - Parser / API
|
|
Shared data plane: AddDamage/AddMiss/..., segments, combat lifecycle,
|
|
GUID helpers. Chat and Nampower backends call Parser methods defined here.
|
|
]]
|
|
|
|
local OM = GreedMeter
|
|
local Parser = {}
|
|
GreedMeter.ParserNS = GreedMeter.ParserNS or {}
|
|
GreedMeter.ParserNS.Parser = Parser
|
|
|
|
-- Lua 5.0 allows only 32 upvalues per function. Keep shared helpers/state on
|
|
-- these tables so large functions (ParseMessage, etc.) close over ~1-2 upvalues.
|
|
local H = {} -- helper bag
|
|
local ST = {} -- fight/parser state bag
|
|
H.getPlayerName = function() return ST.playerName or UnitName("player") end
|
|
H.NormalizeName = function(n) return n end -- replaced after real NormalizeName exists
|
|
|
|
-- ============================================================
|
|
-- Data storage (per segment)
|
|
-- ============================================================
|
|
|
|
OM.data = {
|
|
current = { players = {}, ccTargets = {}, startTime = 0, endTime = 0, label = "Current" },
|
|
overall = { players = {}, ccTargets = {}, startTime = 0, endTime = 0, label = "Overall" },
|
|
-- Last 2 fights of any kind (newest first)
|
|
recentFights = {},
|
|
-- Last 3 boss fights (newest first)
|
|
bossFights = {},
|
|
}
|
|
|
|
local MAX_RECENT_FIGHTS = 3
|
|
local MAX_BOSS_FIGHTS = 5
|
|
|
|
-- Soft caps on per-player detail lists so overall / long sessions stay bounded.
|
|
-- Counts remain accurate; only the stored event history is trimmed (oldest first).
|
|
local MAX_DETAIL_LIST = 80 -- dispels / interrupts / ccBreaks / per-enemy CC applications
|
|
local MAX_DEATHS_LIST = 40
|
|
|
|
-- Per-fight enemy tracking for naming + boss detection (on ST for upvalue relief)
|
|
ST.fightEnemies = {} -- [name] = damage dealt to them by group
|
|
ST.fightEnemyDeaths = {} -- [name] = how many times this name died this fight
|
|
ST.fightDuplicateNames = {} -- [name] = true if name is not unique (pack trash)
|
|
ST.fightEnemyBossLike = {} -- [name] = true if UnitLooksLikeBoss while hitting them
|
|
ST.fightIsBoss = false
|
|
ST.fightBossName = nil
|
|
|
|
-- ============================================================
|
|
-- Helpers
|
|
-- ============================================================
|
|
|
|
ST.playerName = nil
|
|
local playerName = nil -- mirrored; prefer ST.playerName in new code
|
|
|
|
-- Optional mode gates (Advanced Customization "Enabled" checkboxes).
|
|
-- Disabled modes skip storing metrics to save CPU/memory.
|
|
local function ModeEnabled(mode)
|
|
local UI = GreedMeter and GreedMeter.UI
|
|
if UI and UI.IsModeEnabled then
|
|
return UI.IsModeEnabled(mode)
|
|
end
|
|
return true
|
|
end
|
|
|
|
|
|
-- Resolve "You" and SuperWoW-style pet ownership names
|
|
local function NormalizeName(name)
|
|
if not name or name == "" then return nil end
|
|
if type(name) ~= "string" then name = tostring(name) end
|
|
-- Strip residual GUID/color codes if any slipped through
|
|
name = string.gsub(name, "|c%x%x%x%x%x%x%x%x", "")
|
|
name = string.gsub(name, "|r", "")
|
|
name = string.gsub(name, "^%s+", "")
|
|
name = string.gsub(name, "%s+$", "")
|
|
if name == "" then return nil end
|
|
if name == "you" or name == "You" or name == "YOU" then
|
|
return ST.playerName or UnitName("player")
|
|
end
|
|
-- Reject combat-log keywords that bad pattern captures treat as "targets"
|
|
local lower = string.lower(name)
|
|
local BAD = {
|
|
hit=true, hits=true, crit=true, crits=true,
|
|
miss=true, misses=true, missed=true,
|
|
dodge=true, dodges=true, parry=true, parries=true,
|
|
block=true, blocks=true, blocked=true,
|
|
resist=true, resists=true, resisted=true,
|
|
glance=true, glancing=true, crush=true, crushing=true,
|
|
absorb=true, absorbs=true, absorbed=true,
|
|
by=true, from=true, ["for"]=true, with=true, to=true, ["on"]=true,
|
|
you=true, your=true, attack=true, attacks=true,
|
|
of=true, the=true, a=true, an=true, is=true, are=true,
|
|
damage=true, healing=true, heal=true, heals=true, healed=true,
|
|
performs=true, casts=true, gains=true, fades=true,
|
|
suffers=true, afflicted=true, reflects=true, reflected=true,
|
|
partial=true, falls=true, dies=true, slain=true,
|
|
begins=true, able=true, unable=true,
|
|
}
|
|
if BAD[lower] then return nil end
|
|
-- Single-character / pure-number captures are never unit names
|
|
if string.len(name) <= 1 then return nil end
|
|
if tonumber(name) then return nil end
|
|
-- SuperWoW: "PetName (OwnerName)" or "Searing Totem (Shaman)"
|
|
local _, _, pet, owner = string.find(name, "^(.+) %((.+)%)$")
|
|
if pet and owner then
|
|
if OM.SetPetOwner then
|
|
OM:SetPetOwner(pet, owner)
|
|
end
|
|
-- Log already named the owner ("Bear (Alice)"). Use it so two
|
|
-- default-named pets in the group are not collapsed onto one hunter.
|
|
if OM.players and OM.players[owner] then
|
|
return owner
|
|
end
|
|
return pet
|
|
end
|
|
return name
|
|
end
|
|
|
|
|
|
-- Exact match first, then substring (handles rank suffixes / variants).
|
|
-- Used by absorb / interrupt / dispel / CC spell tables.
|
|
H.NormalizeName = NormalizeName
|
|
|
|
local function SpellLookup(set, spell)
|
|
if not spell or not set then return nil end
|
|
local v = set[spell]
|
|
if v ~= nil then return v end
|
|
local name, val
|
|
for name, val in pairs(set) do
|
|
if string.find(spell, name, 1, true) then
|
|
return val
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function SpellInSet(set, spell)
|
|
return SpellLookup(set, spell) ~= nil
|
|
end
|
|
|
|
-- Iterate player + party + raid unit tokens once.
|
|
local function ForEachGroupUnit(fn)
|
|
if not fn then return end
|
|
fn("player")
|
|
local i
|
|
for i = 1, 4 do
|
|
fn("party" .. i)
|
|
end
|
|
if GetNumRaidMembers() > 0 then
|
|
for i = 1, 40 do
|
|
fn("raid" .. i)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- SuperWoW helpers (preferred path when available)
|
|
-- ============================================================
|
|
local guidToName = {}
|
|
local nameToGuid = {}
|
|
|
|
local function SuperWoWAvailable()
|
|
if OM and OM.HasSuperWoW then
|
|
return OM:HasSuperWoW()
|
|
end
|
|
return (SUPERWOW_VERSION or SUPERWOW_STRING or SuperWoW) and true or false
|
|
end
|
|
|
|
local function CacheGuid(name, guid)
|
|
if not name or name == "" or not guid or guid == "" then return end
|
|
-- Never cache combat-log keywords / invalid captures (poisoned GUID maps
|
|
-- showed up as Targets like "hit", "crits", "missed" when Nampower was on).
|
|
name = NormalizeName(name)
|
|
if not name then return end
|
|
guidToName[guid] = name
|
|
nameToGuid[name] = guid
|
|
end
|
|
|
|
|
|
-- Strip SuperWoW GUID tokens from a combat log line while caching them
|
|
-- Handles forms like: 0xF130..., player-..., and trailing GUID chunks
|
|
local function StripAndCacheGuids(msg)
|
|
if not msg then return msg end
|
|
local cleaned = msg
|
|
-- Cache "Name 0xGUID" pairs before stripping (only 0x-prefixed GUIDs)
|
|
local pos = 1
|
|
while true do
|
|
local s, e, name, guid = string.find(cleaned, "([%w'-]+)%s+(0[xX]%x+)", pos)
|
|
if not s then break end
|
|
if string.len(guid) >= 10 then
|
|
CacheGuid(name, guid)
|
|
end
|
|
pos = e + 1
|
|
end
|
|
-- Remove only 0x-prefixed GUID tokens — never bare numbers (those are damage amounts)
|
|
cleaned = string.gsub(cleaned, "%s*0[xX]%x%x%x%x%x%x%x%x+", "")
|
|
return cleaned
|
|
end
|
|
|
|
local function RefreshGuidCacheFromUnits()
|
|
if not SuperWoWAvailable() then return end
|
|
local function scan(unit)
|
|
if not UnitExists(unit) then return end
|
|
local name = UnitName(unit)
|
|
local guid = nil
|
|
if type(UnitGUID) == "function" then
|
|
guid = UnitGUID(unit)
|
|
else
|
|
-- SuperWoW: UnitExists returns GUID as 2nd value
|
|
local ok, g = UnitExists(unit)
|
|
if ok and g and type(g) == "string" and string.len(g) > 4 then
|
|
guid = g
|
|
end
|
|
end
|
|
if name and guid then
|
|
CacheGuid(name, guid)
|
|
end
|
|
-- Pet
|
|
local petUnit = unit
|
|
if unit == "player" then
|
|
petUnit = "pet"
|
|
else
|
|
local _, _, idx = string.find(unit, "^raid(%d+)$")
|
|
if idx then
|
|
petUnit = "raidpet" .. idx
|
|
else
|
|
local _, _, pidx = string.find(unit, "^party(%d+)$")
|
|
if pidx then
|
|
petUnit = "partypet" .. pidx
|
|
else
|
|
petUnit = nil
|
|
end
|
|
end
|
|
end
|
|
if petUnit and UnitExists(petUnit) then
|
|
local pname = UnitName(petUnit)
|
|
local pguid = nil
|
|
if type(UnitGUID) == "function" then
|
|
pguid = UnitGUID(petUnit)
|
|
end
|
|
if pname and pguid then
|
|
CacheGuid(pname, pguid)
|
|
end
|
|
-- Ownership: map pet name to owner for ResolveSource
|
|
if pname and name then
|
|
OM.heuristicPets = OM.heuristicPets or {}
|
|
OM.heuristicPets[pname] = name
|
|
end
|
|
end
|
|
end
|
|
scan("player")
|
|
local i
|
|
for i = 1, 4 do scan("party" .. i) end
|
|
for i = 1, 40 do scan("raid" .. i) end
|
|
end
|
|
|
|
local function EnsurePlayer(segment, name)
|
|
if not name or name == "" then return nil end
|
|
local p = segment.players[name]
|
|
if p then
|
|
-- Refresh class from roster when known
|
|
if OM.players and OM.players[name] and OM.players[name].class then
|
|
p.class = OM.players[name].class
|
|
end
|
|
return p
|
|
end
|
|
local class = nil
|
|
if OM.players and OM.players[name] then
|
|
class = OM.players[name].class
|
|
end
|
|
p = {
|
|
damage = 0,
|
|
healing = 0,
|
|
overhealing = 0,
|
|
rawHealing = 0,
|
|
absorbs = 0,
|
|
damageTaken = 0,
|
|
dispels = { count = 0, list = {} },
|
|
interrupts = { count = 0, list = {} },
|
|
ccBreaks = { count = 0, list = {} },
|
|
deaths = { count = 0, list = {} },
|
|
damageTakenBy = {},
|
|
damageTo = {},
|
|
healingTo = {},
|
|
damageSpells = {},
|
|
healSpells = {},
|
|
damageSpellDetails = {},
|
|
healSpellDetails = {},
|
|
threatCasts = {}, -- non-damaging threat apps (Sunder, Demo Shout, ...)
|
|
class = class,
|
|
}
|
|
segment.players[name] = p
|
|
return p
|
|
end
|
|
|
|
-- Find a unit token for a player name (needed for health deficit / overheal)
|
|
local function FindUnitByName(name)
|
|
if not name then return nil end
|
|
if UnitName("player") == name then return "player" end
|
|
for i = 1, 4 do
|
|
local u = "party"..i
|
|
if UnitExists(u) and UnitName(u) == name then return u end
|
|
end
|
|
if GetNumRaidMembers() > 0 then
|
|
for i = 1, 40 do
|
|
local u = "raid"..i
|
|
if UnitExists(u) and UnitName(u) == name then return u end
|
|
end
|
|
end
|
|
if UnitExists("target") and UnitName("target") == name then return "target" end
|
|
if UnitExists("targettarget") and UnitName("targettarget") == name then return "targettarget" end
|
|
return nil
|
|
end
|
|
|
|
-- Compute effective heal vs overheal using the target's current deficit
|
|
local function SplitOverheal(targetName, amount)
|
|
amount = tonumber(amount) or 0
|
|
if amount <= 0 then return 0, 0 end
|
|
|
|
targetName = NormalizeName(targetName)
|
|
local unit = FindUnitByName(targetName)
|
|
if not unit then
|
|
-- Unknown unit: count full amount as effective (avoid under-counting)
|
|
return amount, 0
|
|
end
|
|
|
|
local maxhp = UnitHealthMax(unit)
|
|
local curhp = UnitHealth(unit)
|
|
if not maxhp or maxhp <= 0 then
|
|
return amount, 0
|
|
end
|
|
|
|
local deficit = maxhp - curhp
|
|
if deficit < 0 then deficit = 0 end
|
|
|
|
local effective = amount
|
|
local over = 0
|
|
if amount > deficit then
|
|
effective = deficit
|
|
over = amount - deficit
|
|
end
|
|
return effective, over
|
|
end
|
|
|
|
local function ResolveSource(name)
|
|
name = NormalizeName(name)
|
|
if not name then return nil end
|
|
|
|
-- Already a group player
|
|
if OM.players[name] then
|
|
return name
|
|
end
|
|
|
|
-- Pet → owner (roster, heuristic, or orphan assignment)
|
|
local owner = OM:ResolvePetOwner(name)
|
|
if owner then
|
|
return owner
|
|
end
|
|
|
|
return name
|
|
end
|
|
|
|
local function IsTracked(name)
|
|
if not name then return false end
|
|
if OM.players[name] then return true end
|
|
-- Known pet names only (unit-token / roster). Never invent ownership here.
|
|
if OM:GetPetOwner(name) then return true end
|
|
return false
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Absorb shield tracking
|
|
-- Credits absorbed damage as healing to the shield provider when known.
|
|
-- ============================================================
|
|
|
|
local ABSORB_SHIELDS = {
|
|
["Power Word: Shield"] = true,
|
|
["Ice Barrier"] = true,
|
|
["Mana Shield"] = true,
|
|
["Sacrifice"] = true,
|
|
["Frost Ward"] = true,
|
|
["Fire Ward"] = true,
|
|
}
|
|
|
|
-- ST.absorbAuras[targetName] = { [shieldName] = applicatorName }
|
|
ST.absorbAuras = {}
|
|
-- ST.recentAbsorbCaster[spellName] = { name = "Caster", time = GetTime() }
|
|
ST.recentAbsorbCaster = {}
|
|
-- ST.recentShieldByTarget[targetName] = { caster = "Caster", spell = "...", time = GetTime() }
|
|
ST.recentShieldByTarget = {}
|
|
local RECENT_CASTER_TIMEOUT = 8
|
|
|
|
local function IsAbsorbShield(spell)
|
|
return SpellInSet(ABSORB_SHIELDS, spell)
|
|
end
|
|
|
|
local function NoteRecentAbsorbCaster(spell, caster, target)
|
|
if not spell or not caster or not IsAbsorbShield(spell) then return end
|
|
caster = NormalizeName(caster)
|
|
ST.recentAbsorbCaster[spell] = {
|
|
name = caster,
|
|
time = GetTime(),
|
|
}
|
|
target = NormalizeName(target)
|
|
if target then
|
|
ST.recentShieldByTarget[target] = {
|
|
caster = caster,
|
|
spell = spell,
|
|
time = GetTime(),
|
|
}
|
|
end
|
|
end
|
|
|
|
local function GetRecentAbsorbCaster(spell)
|
|
if not spell then return nil end
|
|
local entry = ST.recentAbsorbCaster[spell]
|
|
if not entry then
|
|
for s, e in pairs(ST.recentAbsorbCaster) do
|
|
if string.find(spell, s, 1, true) or string.find(s, spell, 1, true) then
|
|
entry = e
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if entry and (GetTime() - entry.time) <= RECENT_CASTER_TIMEOUT then
|
|
return entry.name
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function SetAbsorbAura(target, spell, applicator)
|
|
target = NormalizeName(target)
|
|
applicator = NormalizeName(applicator)
|
|
if not target or not spell or not applicator then return end
|
|
if not ST.absorbAuras[target] then
|
|
ST.absorbAuras[target] = {}
|
|
end
|
|
ST.absorbAuras[target][spell] = applicator
|
|
end
|
|
|
|
local function ClearAbsorbAura(target, spell)
|
|
target = NormalizeName(target)
|
|
if not target or not ST.absorbAuras[target] then return end
|
|
if spell then
|
|
for name, _ in pairs(ST.absorbAuras[target]) do
|
|
if name == spell or string.find(name, spell, 1, true) or string.find(spell, name, 1, true) then
|
|
ST.absorbAuras[target][name] = nil
|
|
end
|
|
end
|
|
local empty = true
|
|
for _ in pairs(ST.absorbAuras[target]) do empty = false break end
|
|
if empty then ST.absorbAuras[target] = nil end
|
|
else
|
|
ST.absorbAuras[target] = nil
|
|
end
|
|
end
|
|
|
|
local function GetAbsorbApplicator(buffedUnit)
|
|
buffedUnit = NormalizeName(buffedUnit)
|
|
if not buffedUnit or not ST.absorbAuras[buffedUnit] then return nil, nil end
|
|
for spell, applicator in pairs(ST.absorbAuras[buffedUnit]) do
|
|
if applicator then
|
|
return applicator, spell
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Recording API
|
|
-- ============================================================
|
|
|
|
-- True when in a dungeon/raid instance (IsInInstance on most 1.12 private servers)
|
|
local function PlayerInInstance()
|
|
if type(IsInInstance) == "function" then
|
|
local inInstance = IsInInstance()
|
|
return inInstance and true or false
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Average max HP of group members (party/raid). Falls back to the player alone.
|
|
local function GetGroupAverageMaxHP()
|
|
local total, count = 0, 0
|
|
ForEachGroupUnit(function(unit)
|
|
if UnitExists(unit) and UnitIsConnected(unit) then
|
|
local hp = UnitHealthMax(unit)
|
|
if hp and hp > 0 then
|
|
total = total + hp
|
|
count = count + 1
|
|
end
|
|
end
|
|
end)
|
|
if count == 0 then
|
|
return UnitHealthMax("player") or 1
|
|
end
|
|
return total / count
|
|
end
|
|
|
|
-- Average player level in the group (for level-gated elite checks)
|
|
local function GetGroupAverageLevel()
|
|
local total, count = 0, 0
|
|
ForEachGroupUnit(function(unit)
|
|
if UnitExists(unit) and UnitIsConnected(unit) then
|
|
local lvl = UnitLevel(unit)
|
|
if lvl and lvl > 0 then
|
|
total = total + lvl
|
|
count = count + 1
|
|
end
|
|
end
|
|
end)
|
|
if count == 0 then return UnitLevel("player") or 1 end
|
|
return total / count
|
|
end
|
|
|
|
local function IsUniqueEnemyName(name)
|
|
if not name then return false end
|
|
if ST.fightDuplicateNames[name] then return false end
|
|
local deaths = ST.fightEnemyDeaths[name] or 0
|
|
-- 2+ deaths of the same name = pack trash, not a unique boss
|
|
if deaths >= 2 then return false end
|
|
return true
|
|
end
|
|
|
|
local function MarkDuplicateName(name)
|
|
if not name then return end
|
|
ST.fightDuplicateNames[name] = true
|
|
-- Clear boss flag for this name if set
|
|
if ST.fightBossName == name then
|
|
ST.fightIsBoss = false
|
|
ST.fightBossName = nil
|
|
-- Try to recover another unique boss candidate from enemies hit
|
|
-- (left for combat-end / next elite hit)
|
|
end
|
|
end
|
|
|
|
local function NoteEnemyDeath(name)
|
|
name = NormalizeName(name)
|
|
if not name or OM.players[name] then return end
|
|
ST.fightEnemyDeaths[name] = (ST.fightEnemyDeaths[name] or 0) + 1
|
|
if ST.fightEnemyDeaths[name] >= 2 then
|
|
MarkDuplicateName(name)
|
|
end
|
|
end
|
|
|
|
local function UnitLooksLikeBoss(unit)
|
|
if not unit or not UnitExists(unit) then return false end
|
|
if UnitIsFriend("player", unit) then return false end
|
|
|
|
local unitName = UnitName(unit)
|
|
-- Pack trash shares names; bosses are unique within the fight
|
|
if unitName and not IsUniqueEnemyName(unitName) then
|
|
return false
|
|
end
|
|
|
|
local classification = UnitClassification(unit)
|
|
local level = UnitLevel(unit)
|
|
|
|
-- Raid / outdoor world bosses
|
|
if classification == "worldboss" then return true end
|
|
-- Skull (??) — typical when the boss is well above the group
|
|
if level == -1 then return true end
|
|
|
|
-- Leveling dungeon bosses: elite/rareelite with a real level, inside an instance
|
|
if classification == "elite" or classification == "rareelite" then
|
|
if PlayerInInstance() then
|
|
local enemyHP = UnitHealthMax(unit) or 0
|
|
local avgHP = GetGroupAverageMaxHP()
|
|
if avgHP < 1 then avgHP = 1 end
|
|
local avgLevel = GetGroupAverageLevel()
|
|
|
|
-- Tanky elite relative to the group's average HP
|
|
if enemyHP >= avgHP * 4 then
|
|
return true
|
|
end
|
|
-- Near group level + meaningful HP pool (covers early dungeon bosses)
|
|
if level > 0 and level >= (avgLevel - 3) and enemyHP >= avgHP * 2.5 then
|
|
return true
|
|
end
|
|
-- Rare elites in instances are almost always notable (if unique)
|
|
if classification == "rareelite" then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function NoteEnemyHit(enemyName, amount)
|
|
enemyName = NormalizeName(enemyName)
|
|
if not enemyName or OM.players[enemyName] then return end
|
|
amount = tonumber(amount) or 0
|
|
ST.fightEnemies[enemyName] = (ST.fightEnemies[enemyName] or 0) + amount
|
|
|
|
-- Never promote non-unique names to boss
|
|
if not IsUniqueEnemyName(enemyName) then
|
|
return
|
|
end
|
|
|
|
-- While the unit is still available, remember if it matches boss criteria
|
|
local units = { "target", "targettarget", "pettarget", "mouseover" }
|
|
local ui
|
|
for ui = 1, table.getn(units) do
|
|
local u = units[ui]
|
|
if UnitExists(u) and UnitName(u) == enemyName and UnitLooksLikeBoss(u) then
|
|
ST.fightEnemyBossLike[enemyName] = true
|
|
ST.fightIsBoss = true
|
|
ST.fightBossName = enemyName
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
local function DeepCopyPlayerData(data)
|
|
local p = {
|
|
damage = data.damage or 0,
|
|
healing = data.healing or 0,
|
|
overhealing = data.overhealing or 0,
|
|
rawHealing = data.rawHealing or 0,
|
|
absorbs = data.absorbs or 0,
|
|
damageTaken = data.damageTaken or 0,
|
|
dispels = { count = 0, list = {} },
|
|
interrupts = { count = 0, list = {} },
|
|
ccBreaks = { count = 0, list = {} },
|
|
deaths = { count = 0, list = {} },
|
|
damageTakenBy = {},
|
|
damageTo = {},
|
|
healingTo = {},
|
|
damageSpells = {},
|
|
healSpells = {},
|
|
damageSpellDetails = {},
|
|
healSpellDetails = {},
|
|
takenSpellDetails = {},
|
|
class = data.class,
|
|
dpsSum = data.dpsSum or 0,
|
|
dpsSamples = data.dpsSamples or 0,
|
|
hpsSum = data.hpsSum or 0,
|
|
hpsSamples = data.hpsSamples or 0,
|
|
}
|
|
if data.damageTo then
|
|
for k, v in pairs(data.damageTo) do
|
|
p.damageTo[k] = v
|
|
end
|
|
end
|
|
if data.healingTo then
|
|
for k, v in pairs(data.healingTo) do
|
|
p.healingTo[k] = v
|
|
end
|
|
end
|
|
if data.dispels then
|
|
p.dispels.count = data.dispels.count or 0
|
|
p.dispels.targets = {}
|
|
if data.dispels.list then
|
|
for i = 1, table.getn(data.dispels.list) do local v = data.dispels.list[i]
|
|
p.dispels.list[i] = v
|
|
end
|
|
end
|
|
if data.dispels.targets then
|
|
for k, v in pairs(data.dispels.targets) do
|
|
p.dispels.targets[k] = v
|
|
end
|
|
end
|
|
end
|
|
if data.interrupts then
|
|
p.interrupts.count = data.interrupts.count or 0
|
|
if data.interrupts.list then
|
|
for i = 1, table.getn(data.interrupts.list) do local v = data.interrupts.list[i]
|
|
p.interrupts.list[i] = v
|
|
end
|
|
end
|
|
end
|
|
if data.ccBreaks then
|
|
p.ccBreaks.count = data.ccBreaks.count or 0
|
|
if data.ccBreaks.list then
|
|
for i = 1, table.getn(data.ccBreaks.list) do local v = data.ccBreaks.list[i]
|
|
if type(v) == "table" then
|
|
p.ccBreaks.list[i] = { spell = v.spell, target = v.target }
|
|
else
|
|
p.ccBreaks.list[i] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if data.deaths then
|
|
p.deaths.count = data.deaths.count or 0
|
|
if data.deaths.list then
|
|
for i = 1, table.getn(data.deaths.list) do local v = data.deaths.list[i]
|
|
if type(v) == "table" then
|
|
p.deaths.list[i] = { killer = v.killer, spell = v.spell, amount = v.amount }
|
|
else
|
|
p.deaths.list[i] = v
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if data.damageTakenBy then
|
|
for k, v in pairs(data.damageTakenBy) do
|
|
p.damageTakenBy[k] = v
|
|
end
|
|
end
|
|
if data.damageSpells then
|
|
for k, v in pairs(data.damageSpells) do
|
|
p.damageSpells[k] = v
|
|
end
|
|
end
|
|
if data.threatCasts then
|
|
p.threatCasts = {}
|
|
for k, v in pairs(data.threatCasts) do
|
|
p.threatCasts[k] = v
|
|
end
|
|
end
|
|
if data.healSpells then
|
|
for k, v in pairs(data.healSpells) do
|
|
p.healSpells[k] = v
|
|
end
|
|
end
|
|
if data.damageSpellDetails then
|
|
local spell, d
|
|
for spell, d in pairs(data.damageSpellDetails) do
|
|
if type(d) == "table" then
|
|
local copy = {
|
|
hits = d.hits or 0, crits = d.crits or 0,
|
|
misses = d.misses or 0, dodges = d.dodges or 0, parries = d.parries or 0,
|
|
blocks = d.blocks or 0, glances = d.glances or 0, crushes = d.crushes or 0,
|
|
resists = d.resists or 0, partials = d.partials or 0,
|
|
total = d.total or 0, count = d.count or 0,
|
|
min = d.min, max = d.max, overheal = d.overheal or 0, byTarget = {},
|
|
}
|
|
if d.byTarget then
|
|
local tn, td
|
|
for tn, td in pairs(d.byTarget) do
|
|
if type(td) == "table" then
|
|
copy.byTarget[tn] = {
|
|
hits = td.hits or 0, crits = td.crits or 0,
|
|
misses = td.misses or 0, dodges = td.dodges or 0, parries = td.parries or 0,
|
|
blocks = td.blocks or 0, glances = td.glances or 0, crushes = td.crushes or 0,
|
|
resists = td.resists or 0, partials = td.partials or 0,
|
|
total = td.total or 0, count = td.count or 0,
|
|
min = td.min, max = td.max,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
p.damageSpellDetails[spell] = copy
|
|
end
|
|
end
|
|
end
|
|
if data.healSpellDetails then
|
|
local spell, d
|
|
for spell, d in pairs(data.healSpellDetails) do
|
|
if type(d) == "table" then
|
|
local copy = {
|
|
hits = d.hits or 0, crits = d.crits or 0,
|
|
misses = d.misses or 0, dodges = d.dodges or 0, parries = d.parries or 0,
|
|
blocks = d.blocks or 0, glances = d.glances or 0, crushes = d.crushes or 0,
|
|
resists = d.resists or 0, partials = d.partials or 0,
|
|
total = d.total or 0, count = d.count or 0,
|
|
min = d.min, max = d.max, overheal = d.overheal or 0, byTarget = {},
|
|
}
|
|
if d.byTarget then
|
|
local tn, td
|
|
for tn, td in pairs(d.byTarget) do
|
|
if type(td) == "table" then
|
|
copy.byTarget[tn] = {
|
|
hits = td.hits or 0, crits = td.crits or 0,
|
|
misses = td.misses or 0, dodges = td.dodges or 0, parries = td.parries or 0,
|
|
blocks = td.blocks or 0, glances = td.glances or 0, crushes = td.crushes or 0,
|
|
resists = td.resists or 0, partials = td.partials or 0,
|
|
total = td.total or 0, count = td.count or 0,
|
|
min = td.min, max = td.max,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
p.healSpellDetails[spell] = copy
|
|
end
|
|
end
|
|
end
|
|
if data.takenSpellDetails then
|
|
local spell, d
|
|
if not p.takenSpellDetails then p.takenSpellDetails = {} end
|
|
for spell, d in pairs(data.takenSpellDetails) do
|
|
if type(d) == "table" then
|
|
local copy = {
|
|
hits = d.hits or 0, crits = d.crits or 0,
|
|
misses = d.misses or 0, dodges = d.dodges or 0, parries = d.parries or 0,
|
|
blocks = d.blocks or 0, glances = d.glances or 0, crushes = d.crushes or 0,
|
|
resists = d.resists or 0, partials = d.partials or 0,
|
|
total = d.total or 0, count = d.count or 0,
|
|
min = d.min, max = d.max, overheal = d.overheal or 0, byTarget = {},
|
|
}
|
|
if d.byTarget then
|
|
local tn, td
|
|
for tn, td in pairs(d.byTarget) do
|
|
if type(td) == "table" then
|
|
copy.byTarget[tn] = {
|
|
hits = td.hits or 0, crits = td.crits or 0,
|
|
misses = td.misses or 0, dodges = td.dodges or 0, parries = td.parries or 0,
|
|
blocks = td.blocks or 0, glances = td.glances or 0, crushes = td.crushes or 0,
|
|
resists = td.resists or 0, partials = td.partials or 0,
|
|
total = td.total or 0, count = td.count or 0,
|
|
min = td.min, max = td.max,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
p.takenSpellDetails[spell] = copy
|
|
end
|
|
end
|
|
end
|
|
return p
|
|
end
|
|
|
|
|
|
local function SnapshotSegment(seg)
|
|
local players = {}
|
|
if seg and seg.players then
|
|
for name, data in pairs(seg.players) do
|
|
players[name] = DeepCopyPlayerData(data)
|
|
end
|
|
end
|
|
local ccTargets = {}
|
|
if seg and seg.ccTargets then
|
|
for enemy, data in pairs(seg.ccTargets) do
|
|
local entry = { count = data.count or 0, duration = data.duration or 0, list = {} }
|
|
if data.list then
|
|
local i, v
|
|
for i = 1, table.getn(data.list) do local v = data.list[i]
|
|
if type(v) == "table" then
|
|
entry.list[i] = { spell = v.spell, duration = v.duration }
|
|
end
|
|
end
|
|
end
|
|
ccTargets[enemy] = entry
|
|
end
|
|
end
|
|
return {
|
|
players = players,
|
|
ccTargets = ccTargets,
|
|
startTime = seg and seg.startTime or 0,
|
|
endTime = seg and seg.endTime or 0,
|
|
label = seg and seg.label or "Fight",
|
|
isBoss = seg and seg.isBoss or false,
|
|
duration = seg and seg.duration or 0,
|
|
}
|
|
end
|
|
|
|
-- At segment save: any damaged enemy that looked like a boss → boss segment
|
|
local function ResolveBossFromDamagedEnemies(seg)
|
|
local totals = {}
|
|
local name, dmg
|
|
|
|
if ST.fightEnemies then
|
|
for name, dmg in pairs(ST.fightEnemies) do
|
|
totals[name] = (totals[name] or 0) + (dmg or 0)
|
|
end
|
|
end
|
|
-- Also fold per-player damageTo (covers any path that skipped NoteEnemyHit)
|
|
if seg and seg.players then
|
|
local _, pdata
|
|
for _, pdata in pairs(seg.players) do
|
|
if pdata and pdata.damageTo then
|
|
for name, dmg in pairs(pdata.damageTo) do
|
|
if name and not (OM.players and OM.players[name]) then
|
|
totals[name] = (totals[name] or 0) + (dmg or 0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local bestName, bestDmg = nil, -1
|
|
local units = { "target", "targettarget", "pettarget", "mouseover" }
|
|
for name, dmg in pairs(totals) do
|
|
if IsUniqueEnemyName(name) then
|
|
local bossLike = ST.fightEnemyBossLike and ST.fightEnemyBossLike[name]
|
|
if not bossLike then
|
|
local ui
|
|
for ui = 1, table.getn(units) do
|
|
local u = units[ui]
|
|
if UnitExists(u) and UnitName(u) == name and UnitLooksLikeBoss(u) then
|
|
bossLike = true
|
|
if ST.fightEnemyBossLike then
|
|
ST.fightEnemyBossLike[name] = true
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if bossLike and dmg > bestDmg then
|
|
bestDmg = dmg
|
|
bestName = name
|
|
end
|
|
end
|
|
end
|
|
|
|
if bestName then
|
|
ST.fightIsBoss = true
|
|
ST.fightBossName = bestName
|
|
else
|
|
ST.fightIsBoss = false
|
|
ST.fightBossName = nil
|
|
end
|
|
end
|
|
|
|
local function PickFightLabel()
|
|
if ST.fightBossName then
|
|
return ST.fightBossName
|
|
end
|
|
local bestName, bestDmg = nil, -1
|
|
for name, dmg in pairs(ST.fightEnemies) do
|
|
if dmg > bestDmg then
|
|
bestDmg = dmg
|
|
bestName = name
|
|
end
|
|
end
|
|
if bestName then return bestName end
|
|
return "Fight"
|
|
end
|
|
|
|
local function PushFront(list, entry, maxCount)
|
|
table.insert(list, 1, entry)
|
|
while table.getn(list) > maxCount do
|
|
table.remove(list)
|
|
end
|
|
end
|
|
|
|
-- Append then drop oldest when over the soft cap (chronological history).
|
|
local function PushCapped(list, entry, maxCount)
|
|
table.insert(list, entry)
|
|
while table.getn(list) > maxCount do
|
|
table.remove(list, 1)
|
|
end
|
|
end
|
|
|
|
-- Last damage dealt TO a unit (for CC break attribution + death killing blow)
|
|
-- ST.lastHitOn[target] = { source, spell, amount, time }
|
|
ST.lastHitOn = {}
|
|
|
|
local function NoteLastHit(target, source, spell, amount)
|
|
target = NormalizeName(target)
|
|
source = NormalizeName(source)
|
|
if not target then return end
|
|
ST.lastHitOn[target] = {
|
|
source = source or "Unknown",
|
|
spell = spell or "Melee",
|
|
amount = amount or 0,
|
|
time = GetTime(),
|
|
}
|
|
end
|
|
|
|
-- Stamp last combat activity on the current segment (for trailing-idle duration trim)
|
|
-- Begin a meter combat segment before the first event is stored (pre-REGEN hits).
|
|
local function EnsureInCombat()
|
|
if OM and not OM.inCombat and OM.StartCombat then
|
|
OM:StartCombat()
|
|
end
|
|
end
|
|
|
|
local function NoteActivity()
|
|
if OM.data and OM.data.current then
|
|
local now = GetTime()
|
|
OM.data.current.lastActivityTime = now
|
|
local st = OM.data.current.startTime or 0
|
|
if st <= 0 then
|
|
OM.data.current.startTime = now
|
|
end
|
|
-- Overall session clock starts on first combat activity
|
|
if OM.data.overall then
|
|
local ost = OM.data.overall.startTime or 0
|
|
if ost <= 0 then
|
|
OM.data.overall.startTime = now
|
|
end
|
|
OM.data.overall.lastActivityTime = now
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Abilities that generate threat with little or no damage component.
|
|
-- Counted from cast/perform combat-log lines for estimate use.
|
|
local THREAT_CAST_ABILITIES = {
|
|
["Sunder Armor"] = true,
|
|
["Demoralizing Shout"] = true,
|
|
["Demoralizing Roar"] = true,
|
|
["Faerie Fire"] = true,
|
|
["Faerie Fire (Feral)"] = true,
|
|
["Hamstring"] = true,
|
|
-- Pet / minion high-threat abilities (1.12)
|
|
["Growl"] = true, -- hunter pet
|
|
["Intimidation"] = true, -- hunter BM talent (threat on pet)
|
|
["Torment"] = true, -- voidwalker
|
|
["Suffering"] = true, -- voidwalker AoE threat
|
|
}
|
|
|
|
local function IsThreatCastAbility(spell)
|
|
if not spell then return false end
|
|
if THREAT_CAST_ABILITIES[spell] then return true end
|
|
local k
|
|
for k in pairs(THREAT_CAST_ABILITIES) do
|
|
if string.find(spell, k, 1, true) then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function Parser:AddThreatCast(source, spell)
|
|
if not source or not spell then return end
|
|
if not IsThreatCastAbility(spell) then return end
|
|
source = ResolveSource(source)
|
|
if not source then return end
|
|
if not IsTracked(source) and not (OM.players and OM.players[source]) then return end
|
|
|
|
local function apply(seg)
|
|
if not seg then return end
|
|
local p = EnsurePlayer(seg, source)
|
|
if not p then return end
|
|
if not p.threatCasts then p.threatCasts = {} end
|
|
p.threatCasts[spell] = (p.threatCasts[spell] or 0) + 1
|
|
end
|
|
apply(OM.data and OM.data.current)
|
|
apply(OM.data and OM.data.overall)
|
|
end
|
|
|
|
local function EmptyOutcomeBucket()
|
|
return {
|
|
hits = 0, crits = 0, misses = 0, dodges = 0, parries = 0,
|
|
blocks = 0, glances = 0, crushes = 0,
|
|
resists = 0, partials = 0,
|
|
total = 0, overheal = 0, count = 0, min = nil, max = nil,
|
|
}
|
|
end
|
|
|
|
local function ApplyOutcomeToBucket(s, amount, hitType, partialFlag, isTaken)
|
|
if not s then return end
|
|
-- isTaken: when false/nil, dodge+parry also increment misses so damage-done
|
|
-- detail "Miss" column includes them. Taken mode keeps separate columns.
|
|
if hitType == "miss" then
|
|
s.misses = (s.misses or 0) + 1
|
|
return
|
|
elseif hitType == "dodge" then
|
|
s.dodges = (s.dodges or 0) + 1
|
|
if not isTaken then
|
|
s.misses = (s.misses or 0) + 1
|
|
end
|
|
return
|
|
elseif hitType == "parry" then
|
|
s.parries = (s.parries or 0) + 1
|
|
if not isTaken then
|
|
s.misses = (s.misses or 0) + 1
|
|
end
|
|
return
|
|
elseif hitType == "block" then
|
|
-- Full avoid block (attack was blocked with no damage line)
|
|
s.blocks = (s.blocks or 0) + 1
|
|
return
|
|
elseif hitType == "resist" then
|
|
s.resists = (s.resists or 0) + 1
|
|
return
|
|
elseif hitType == "crit" then
|
|
s.crits = (s.crits or 0) + 1
|
|
elseif hitType == "glance" then
|
|
s.glances = (s.glances or 0) + 1
|
|
elseif hitType == "crush" then
|
|
s.crushes = (s.crushes or 0) + 1
|
|
else
|
|
s.hits = (s.hits or 0) + 1
|
|
end
|
|
if partialFlag then
|
|
s.partials = (s.partials or 0) + 1
|
|
end
|
|
amount = tonumber(amount) or 0
|
|
if amount > 0 then
|
|
s.total = (s.total or 0) + amount
|
|
s.count = (s.count or 0) + 1
|
|
if s.min == nil or amount < s.min then s.min = amount end
|
|
if s.max == nil or amount > s.max then s.max = amount end
|
|
end
|
|
end
|
|
|
|
local function NoteSpellOutcome(p, spell, amount, hitType, isHeal, target, partialFlag, isTaken, overhealAmount)
|
|
if not p or not spell or spell == "" then return end
|
|
hitType = hitType or "hit"
|
|
overhealAmount = tonumber(overhealAmount) or 0
|
|
local bagName
|
|
if isTaken then
|
|
bagName = "takenSpellDetails"
|
|
elseif isHeal then
|
|
bagName = "healSpellDetails"
|
|
else
|
|
bagName = "damageSpellDetails"
|
|
end
|
|
if not p[bagName] then p[bagName] = {} end
|
|
local s = p[bagName][spell]
|
|
if not s then
|
|
s = EmptyOutcomeBucket()
|
|
s.byTarget = {}
|
|
p[bagName][spell] = s
|
|
end
|
|
if not s.byTarget then s.byTarget = {} end
|
|
ApplyOutcomeToBucket(s, amount, hitType, partialFlag, isTaken)
|
|
if overhealAmount > 0 then
|
|
s.overheal = (s.overheal or 0) + overhealAmount
|
|
end
|
|
if target and target ~= "" then
|
|
local ts = s.byTarget[target]
|
|
if not ts then
|
|
ts = EmptyOutcomeBucket()
|
|
s.byTarget[target] = ts
|
|
end
|
|
ApplyOutcomeToBucket(ts, amount, hitType, partialFlag, isTaken)
|
|
if overhealAmount > 0 then
|
|
ts.overheal = (ts.overheal or 0) + overhealAmount
|
|
end
|
|
end
|
|
end
|
|
|
|
function Parser:AddDamage(source, amount, spell, target, hitType, partialFlag, isPeriodic)
|
|
EnsureInCombat()
|
|
local isPet = false
|
|
local rawSource = source
|
|
if rawSource and string.find(rawSource, " %(.+%)$") then
|
|
isPet = true
|
|
end
|
|
local rawName = rawSource and NormalizeName(rawSource) or rawSource
|
|
local isTotem = rawName and OM.IsTotemName and OM:IsTotemName(rawName)
|
|
if rawName and rawName ~= "You" and rawName ~= "you" then
|
|
if OM.GetPetOwner and OM:GetPetOwner(rawName) then
|
|
isPet = true
|
|
elseif OM.heuristicPets and OM.heuristicPets[rawName] then
|
|
isPet = true
|
|
end
|
|
end
|
|
if isTotem and (not spell or spell == "" or spell == "Attack" or spell == "Auto Attack") then
|
|
spell = rawName
|
|
end
|
|
|
|
source = ResolveSource(source)
|
|
-- Pets always attribute to their owner on the meter (never their own bar).
|
|
-- mergePetDamage only controls tooltip spell breakdown (one line vs per-ability).
|
|
if source and OM.GetPetOwner then
|
|
local owner = OM:GetPetOwner(source)
|
|
if owner then
|
|
isPet = true
|
|
source = owner
|
|
end
|
|
end
|
|
if not source or not amount or amount <= 0 then return end
|
|
-- Only record damage from tracked group members (pets resolve to owners above)
|
|
if not OM.players[source] then
|
|
return
|
|
end
|
|
|
|
if isPet and not isTotem then
|
|
if OM.GetSetting and OM:GetSetting("mergePetDamage") then
|
|
spell = "Pet: Damage"
|
|
else
|
|
if not spell or spell == "" then
|
|
spell = "Auto Attack"
|
|
end
|
|
if string.sub(spell, 1, 5) ~= "Pet: " then
|
|
spell = "Pet: " .. spell
|
|
end
|
|
end
|
|
end
|
|
|
|
target = NormalizeName(target)
|
|
|
|
if ModeEnabled("damage") then
|
|
local p = EnsurePlayer(OM.data.current, source)
|
|
if p then
|
|
p.damage = p.damage + amount
|
|
if spell then
|
|
p.damageSpells[spell] = (p.damageSpells[spell] or 0) + amount
|
|
NoteSpellOutcome(p, spell, amount, hitType or "hit", false, target, partialFlag)
|
|
end
|
|
if target then
|
|
p.damageTo[target] = (p.damageTo[target] or 0) + amount
|
|
NoteLastHit(target, source, spell, amount)
|
|
if not OM.players[target] then
|
|
NoteEnemyHit(target, amount)
|
|
end
|
|
end
|
|
end
|
|
|
|
local o = EnsurePlayer(OM.data.overall, source)
|
|
if o then
|
|
o.damage = o.damage + amount
|
|
if spell then
|
|
o.damageSpells[spell] = (o.damageSpells[spell] or 0) + amount
|
|
NoteSpellOutcome(o, spell, amount, hitType or "hit", false, target, partialFlag)
|
|
end
|
|
if target then
|
|
o.damageTo[target] = (o.damageTo[target] or 0) + amount
|
|
end
|
|
end
|
|
elseif target then
|
|
-- Still track last-hit for death tooltips even when damage mode is off
|
|
NoteLastHit(target, source, spell, amount)
|
|
end
|
|
-- Interrupt abilities: combat log rarely reports "interrupted X"; count uses instead
|
|
if ModeEnabled("interrupts") and spell and Parser.IsInterruptAbility and Parser.IsInterruptAbility(spell) then
|
|
Parser:AddInterrupt(source, spell)
|
|
end
|
|
-- Breakable CC: first damage to a CC'd target credits the breaker
|
|
if ModeEnabled("ccbreak") and target and Parser.NoteBreakableCCDamage then
|
|
Parser.NoteBreakableCCDamage(target, source, spell)
|
|
end
|
|
NoteActivity()
|
|
end
|
|
|
|
-- amount = full heal from the log
|
|
-- healing field stores EFFECTIVE healing (amount - overheal)
|
|
-- overhealing tracked separately for later tooltip %
|
|
function Parser:AddMiss(source, spell, target, avoidType)
|
|
EnsureInCombat()
|
|
-- avoidType: "miss" (default), "dodge", "parry", "block"
|
|
-- Outbound avoids (swing/spell avoided by the target).
|
|
if not ModeEnabled("damage") then return end
|
|
local me = ST.playerName or UnitName("player")
|
|
if source == "You" or source == "you" or source == nil or source == "" then
|
|
source = me
|
|
end
|
|
source = ResolveSource(source) or source or me
|
|
if not source or source == "" then
|
|
source = me
|
|
end
|
|
if not source then return end
|
|
-- Always accept the local player (case-insensitive); group members via roster
|
|
local sourceOK = false
|
|
if me and string.lower(tostring(source)) == string.lower(tostring(me)) then
|
|
source = me
|
|
sourceOK = true
|
|
elseif OM.players and OM.players[source] then
|
|
sourceOK = true
|
|
elseif IsTracked and IsTracked(source) then
|
|
sourceOK = true
|
|
end
|
|
if not sourceOK then return end
|
|
spell = spell or "Auto Attack"
|
|
target = NormalizeName(target)
|
|
avoidType = avoidType or "miss"
|
|
local function apply(seg)
|
|
if not seg then return end
|
|
local p = EnsurePlayer(seg, source)
|
|
if p then
|
|
NoteSpellOutcome(p, spell, 0, avoidType, false, target, false)
|
|
end
|
|
end
|
|
apply(OM.data.current)
|
|
apply(OM.data.overall)
|
|
NoteActivity()
|
|
end
|
|
|
|
function Parser:AddBlock(source, spell, target)
|
|
EnsureInCombat()
|
|
source = ResolveSource(source)
|
|
if not source then return end
|
|
if not OM.players[source] and not IsTracked(source) then return end
|
|
spell = spell or "Auto Attack"
|
|
target = NormalizeName(target)
|
|
local function apply(seg)
|
|
if not seg then return end
|
|
local p = EnsurePlayer(seg, source)
|
|
if p then
|
|
NoteSpellOutcome(p, spell, 0, "block", false, target, false)
|
|
end
|
|
end
|
|
apply(OM.data and OM.data.current)
|
|
apply(OM.data and OM.data.overall)
|
|
end
|
|
|
|
function Parser:AddResist(source, spell, target)
|
|
EnsureInCombat()
|
|
source = ResolveSource(source)
|
|
if not source then return end
|
|
if not OM.players[source] and not IsTracked(source) then return end
|
|
spell = spell or "Unknown"
|
|
target = NormalizeName(target)
|
|
local function apply(seg)
|
|
if not seg then return end
|
|
local p = EnsurePlayer(seg, source)
|
|
if p then
|
|
NoteSpellOutcome(p, spell, 0, "resist", false, target, false)
|
|
end
|
|
end
|
|
apply(OM.data and OM.data.current)
|
|
apply(OM.data and OM.data.overall)
|
|
end
|
|
|
|
function Parser:AddHealing(source, amount, spell, isAbsorb, target, hitType)
|
|
EnsureInCombat()
|
|
if not ModeEnabled("healing") then return end
|
|
local rawName = source and NormalizeName(source) or source
|
|
if rawName and OM.IsTotemName and OM:IsTotemName(rawName) then
|
|
if not spell or spell == "" then
|
|
spell = rawName
|
|
end
|
|
end
|
|
source = ResolveSource(source)
|
|
if not source or not amount or amount <= 0 then return end
|
|
if not IsTracked(source) and not OM.players[source] then return end
|
|
|
|
local effective, over = amount, 0
|
|
|
|
if isAbsorb then
|
|
-- Absorbs are always fully "effective" (damage prevented)
|
|
effective, over = amount, 0
|
|
else
|
|
effective, over = SplitOverheal(target, amount)
|
|
end
|
|
|
|
local healTarget = NormalizeName(target)
|
|
|
|
local function apply(seg)
|
|
local p = EnsurePlayer(seg, source)
|
|
if not p then return end
|
|
p.rawHealing = (p.rawHealing or 0) + amount
|
|
p.overhealing = (p.overhealing or 0) + over
|
|
local credited = isAbsorb and amount or effective
|
|
if isAbsorb then
|
|
p.absorbs = (p.absorbs or 0) + amount
|
|
p.healing = (p.healing or 0) + amount -- absorbs add to effective total
|
|
else
|
|
p.healing = (p.healing or 0) + effective
|
|
end
|
|
if spell then
|
|
p.healSpells[spell] = (p.healSpells[spell] or 0) + credited
|
|
NoteSpellOutcome(p, spell, credited, hitType or "hit", true, healTarget, false, nil, over)
|
|
end
|
|
if healTarget and credited > 0 then
|
|
p.healingTo[healTarget] = (p.healingTo[healTarget] or 0) + credited
|
|
end
|
|
end
|
|
|
|
apply(OM.data.current)
|
|
apply(OM.data.overall)
|
|
NoteActivity()
|
|
end
|
|
|
|
function Parser:AddDamageTaken(target, amount, source, spell, hitType, partialFlag, isPeriodic)
|
|
EnsureInCombat()
|
|
if not ModeEnabled("taken") then return end
|
|
target = NormalizeName(target)
|
|
amount = tonumber(amount) or 0
|
|
if not target or amount <= 0 then return end
|
|
if not OM.players[target] then return end -- only track damage taken by the group
|
|
|
|
source = NormalizeName(source) or "Unknown"
|
|
spell = spell or "Auto Attack"
|
|
hitType = hitType or "hit"
|
|
NoteLastHit(target, source, spell, amount)
|
|
|
|
local function apply(seg)
|
|
if not seg then return end
|
|
local p = EnsurePlayer(seg, target)
|
|
if not p then return end
|
|
p.damageTaken = (p.damageTaken or 0) + amount
|
|
if not p.damageTakenBy then p.damageTakenBy = {} end
|
|
p.damageTakenBy[source] = (p.damageTakenBy[source] or 0) + amount
|
|
-- byTarget on taken details = attacker name
|
|
NoteSpellOutcome(p, spell, amount, hitType, false, source, partialFlag, true)
|
|
end
|
|
apply(OM.data and OM.data.current)
|
|
apply(OM.data and OM.data.overall)
|
|
-- DoT ticks still count for meters, but must not keep the segment alive
|
|
if not isPeriodic then
|
|
NoteActivity()
|
|
end
|
|
end
|
|
|
|
-- Incoming avoid (enemy missed / dodge-parry-block): no damage amount
|
|
function Parser:AddTakenAvoid(target, spell, source, avoidType)
|
|
EnsureInCombat()
|
|
if not ModeEnabled("taken") then return end
|
|
target = NormalizeName(target)
|
|
if not target or not OM.players[target] then return end
|
|
source = NormalizeName(source) or "Unknown"
|
|
spell = spell or "Auto Attack"
|
|
avoidType = avoidType or "miss"
|
|
local function apply(seg)
|
|
if not seg then return end
|
|
local p = EnsurePlayer(seg, target)
|
|
if p then
|
|
NoteSpellOutcome(p, spell, 0, avoidType, false, source, false, true)
|
|
end
|
|
end
|
|
apply(OM.data and OM.data.current)
|
|
apply(OM.data and OM.data.overall)
|
|
NoteActivity()
|
|
end
|
|
|
|
function Parser:AddDispel(source, what, target)
|
|
if not ModeEnabled("dispels") then return end
|
|
source = ResolveSource(source)
|
|
if not source then return end
|
|
-- Prefer group members
|
|
if not IsTracked(source) and not OM.players[source] then return end
|
|
what = what or "Unknown"
|
|
target = NormalizeName(target)
|
|
|
|
local function apply(seg)
|
|
local p = EnsurePlayer(seg, source)
|
|
if not p then return end
|
|
p.dispels.count = p.dispels.count + 1
|
|
PushCapped(p.dispels.list, what, MAX_DETAIL_LIST)
|
|
if not p.dispels.targets then
|
|
p.dispels.targets = {}
|
|
end
|
|
if target and target ~= "" then
|
|
p.dispels.targets[target] = (p.dispels.targets[target] or 0) + 1
|
|
end
|
|
end
|
|
apply(OM.data.current)
|
|
apply(OM.data.overall)
|
|
end
|
|
|
|
function Parser:AddInterrupt(source, what)
|
|
if not ModeEnabled("interrupts") then return end
|
|
source = ResolveSource(source)
|
|
if not source then return end
|
|
if not IsTracked(source) and not OM.players[source] then return end
|
|
local p = EnsurePlayer(OM.data.current, source)
|
|
if p then
|
|
p.interrupts.count = p.interrupts.count + 1
|
|
PushCapped(p.interrupts.list, what or "Unknown", MAX_DETAIL_LIST)
|
|
end
|
|
local o = EnsurePlayer(OM.data.overall, source)
|
|
if o then
|
|
o.interrupts.count = o.interrupts.count + 1
|
|
PushCapped(o.interrupts.list, what or "Unknown", MAX_DETAIL_LIST)
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- CC tracking (enemy-centric) + breakable CC breaks (player-centric)
|
|
-- Combat log usually says "Enemy is afflicted by Sap" with no caster.
|
|
-- Lists enemies that were CC'd and estimates full duration.
|
|
-- Breakable CCs (Sap/Gouge/Blind/Poly/Frost Trap): first damage to the
|
|
-- target after the CC is applied credits the breaker.
|
|
-- ============================================================
|
|
|
|
-- Breakable (damage-breakable) hard CCs
|
|
local BREAKABLE_CC = {
|
|
["Sap"] = true,
|
|
["Gouge"] = true,
|
|
["Blind"] = true,
|
|
["Polymorph"] = true,
|
|
["Polymorph: Pig"] = true,
|
|
["Polymorph: Turtle"] = true,
|
|
["Freezing Trap Effect"] = true,
|
|
["Freezing Trap"] = true,
|
|
["Hibernate"] = true,
|
|
["Shackle Undead"] = true,
|
|
["Wyvern Sting"] = true,
|
|
["Seduction"] = true,
|
|
["Scare Beast"] = true,
|
|
}
|
|
|
|
-- activeBreakable[target] = { spell = "...", time = GetTime() }
|
|
local activeBreakable = {}
|
|
|
|
local function IsBreakableCC(spell)
|
|
return SpellInSet(BREAKABLE_CC, spell)
|
|
end
|
|
|
|
local function EnsureCCTargets(seg)
|
|
if not seg.ccTargets then
|
|
seg.ccTargets = {}
|
|
end
|
|
return seg.ccTargets
|
|
end
|
|
|
|
local function EnsureCCTargetEntry(seg, enemy)
|
|
local t = EnsureCCTargets(seg)
|
|
if not t[enemy] then
|
|
t[enemy] = { count = 0, duration = 0, list = {} }
|
|
end
|
|
return t[enemy]
|
|
end
|
|
|
|
-- Record that an enemy was CC'd (no caster required). Duration = estimate.
|
|
function Parser:AddEnemyCC(spell, target, maxDuration)
|
|
if not ModeEnabled("cc") then return end
|
|
target = NormalizeName(target)
|
|
if not target or target == "" then return end
|
|
-- Do not track the player's group as CC targets in this mode
|
|
if OM.players and OM.players[target] then return end
|
|
-- Guard against combat-log fragment targets ("on", "with", etc.)
|
|
if string.len(target) < 3 then return end
|
|
|
|
spell = spell or "Unknown"
|
|
-- Reject fragment spell names
|
|
local sl = string.lower(spell)
|
|
if sl == "on" or sl == "with" or sl == "by" or sl == "from" or string.len(spell) < 3 then
|
|
return
|
|
end
|
|
maxDuration = tonumber(maxDuration) or 0
|
|
if maxDuration < 0 then maxDuration = 0 end
|
|
|
|
local function apply(seg)
|
|
local e = EnsureCCTargetEntry(seg, target)
|
|
e.count = e.count + 1
|
|
e.duration = (e.duration or 0) + maxDuration
|
|
PushCapped(e.list, { spell = spell, duration = maxDuration }, MAX_DETAIL_LIST)
|
|
end
|
|
apply(OM.data.current)
|
|
apply(OM.data.overall)
|
|
|
|
if IsBreakableCC(spell) then
|
|
activeBreakable[target] = { spell = spell, time = GetTime() }
|
|
end
|
|
end
|
|
|
|
-- First damage to a breakable-CC'd target → credit the breaker
|
|
local function NoteBreakableCCDamage(target, source, spell)
|
|
target = NormalizeName(target)
|
|
if not target then return end
|
|
local entry = activeBreakable[target]
|
|
if not entry then return end
|
|
-- Ignore tiny window noise right as CC is applied
|
|
if entry.time and (GetTime() - entry.time) < 0.15 then return end
|
|
activeBreakable[target] = nil
|
|
Parser:AddCCBreak(source, entry.spell, target)
|
|
end
|
|
Parser.NoteBreakableCCDamage = NoteBreakableCCDamage
|
|
|
|
function Parser:FinishCC(target, spell)
|
|
target = NormalizeName(target)
|
|
if target then
|
|
activeBreakable[target] = nil
|
|
end
|
|
end
|
|
|
|
function Parser:FlushActiveCCs()
|
|
activeBreakable = {}
|
|
end
|
|
|
|
function Parser:AddCCBreak(breaker, ccSpell, target)
|
|
if not ModeEnabled("ccbreak") then return end
|
|
breaker = ResolveSource(breaker)
|
|
if not breaker then return end
|
|
if not IsTracked(breaker) and not OM.players[breaker] then return end
|
|
ccSpell = ccSpell or "CC"
|
|
target = NormalizeName(target) or "?"
|
|
|
|
local p = EnsurePlayer(OM.data.current, breaker)
|
|
if p then
|
|
p.ccBreaks.count = p.ccBreaks.count + 1
|
|
PushCapped(p.ccBreaks.list, { spell = ccSpell, target = target }, MAX_DETAIL_LIST)
|
|
end
|
|
local o = EnsurePlayer(OM.data.overall, breaker)
|
|
if o then
|
|
o.ccBreaks.count = o.ccBreaks.count + 1
|
|
PushCapped(o.ccBreaks.list, { spell = ccSpell, target = target }, MAX_DETAIL_LIST)
|
|
end
|
|
end
|
|
|
|
|
|
function Parser:AddDeath(name, lastHit)
|
|
if not ModeEnabled("deaths") then return end
|
|
name = NormalizeName(name)
|
|
if not name then return end
|
|
if not OM.players[name] and name ~= (playerName or UnitName("player")) then
|
|
return
|
|
end
|
|
|
|
local killer, spell, amount = "?", "?", 0
|
|
if type(lastHit) == "table" then
|
|
killer = lastHit.source or lastHit.killer or "?"
|
|
spell = lastHit.spell or "?"
|
|
amount = tonumber(lastHit.amount) or 0
|
|
end
|
|
|
|
local function apply(seg)
|
|
local p = EnsurePlayer(seg, name)
|
|
if not p then return end
|
|
p.deaths.count = (p.deaths.count or 0) + 1
|
|
if not p.deaths.list then p.deaths.list = {} end
|
|
PushCapped(p.deaths.list, {
|
|
killer = killer,
|
|
spell = spell,
|
|
amount = amount,
|
|
}, MAX_DEATHS_LIST)
|
|
end
|
|
apply(OM.data.current)
|
|
apply(OM.data.overall)
|
|
end
|
|
|
|
-- ============================================================
|
|
|
|
H.EnsurePlayer = EnsurePlayer
|
|
H.IsUniqueEnemyName = IsUniqueEnemyName
|
|
H.UnitLooksLikeBoss = UnitLooksLikeBoss
|
|
H.PickFightLabel = PickFightLabel
|
|
H.PushFront = PushFront
|
|
H.SnapshotSegment = SnapshotSegment
|
|
H.DeepCopyPlayerData = DeepCopyPlayerData
|
|
H.MAX_RECENT_FIGHTS = MAX_RECENT_FIGHTS
|
|
H.MAX_BOSS_FIGHTS = MAX_BOSS_FIGHTS
|
|
H.NoteLastHit = NoteLastHit
|
|
H.NoteActivity = NoteActivity
|
|
H.EnsureInCombat = EnsureInCombat
|
|
H.ModeEnabled = ModeEnabled
|
|
H.ST = ST
|
|
H.OM = OM
|
|
H.Parser = Parser
|
|
H.getPlayerName = function() return ST.playerName or UnitName("player") end
|
|
H.NormalizeName = NormalizeName
|
|
H.ResolveSource = ResolveSource
|
|
H.NoteEnemyHit = NoteEnemyHit
|
|
H.ResolveBossFromDamagedEnemies = ResolveBossFromDamagedEnemies
|
|
|
|
-- ClearDispelBuffers is defined in Backend_Chat; resolve at call time
|
|
H.ClearDispelBuffers = function()
|
|
local NS = GreedMeter.ParserNS
|
|
if NS and NS.ClearDispelBuffers then
|
|
NS.ClearDispelBuffers()
|
|
end
|
|
end
|
|
|
|
function Parser:OnCombatStart()
|
|
local P = H.Parser
|
|
local S = H.ST
|
|
local O = H.OM
|
|
P:FlushActiveCCs()
|
|
S.lastHitOn = {}
|
|
S.fightEnemies = {}
|
|
S.fightEnemyDeaths = {}
|
|
S.fightDuplicateNames = {}
|
|
S.fightEnemyBossLike = {}
|
|
S.fightIsBoss = false
|
|
S.fightBossName = nil
|
|
H.ClearDispelBuffers()
|
|
S.recentHarmfulAuras = {}
|
|
|
|
-- Always start Current empty. Keeping the previous players table to catch
|
|
-- pre-pull hits was too aggressive (late combat-log lines after REGEN_ENABLED
|
|
-- set lastActivityTime past endTime, so the next fight reused the old totals).
|
|
-- Opening-hit timing is handled by backdating startTime on the first event
|
|
-- after combat starts (see NoteActivity / first damage).
|
|
local now = GetTime()
|
|
O.data.current = {
|
|
players = {},
|
|
ccTargets = {},
|
|
startTime = now,
|
|
endTime = 0,
|
|
label = "Current",
|
|
isBoss = false,
|
|
duration = 0,
|
|
lastActivityTime = nil,
|
|
}
|
|
end
|
|
|
|
function Parser:OnCombatEnd(duration)
|
|
local P = H.Parser
|
|
local O = H.OM
|
|
local S = H.ST
|
|
local EnsurePlayer = H.EnsurePlayer
|
|
local IsUniqueEnemyName = H.IsUniqueEnemyName
|
|
local UnitLooksLikeBoss = H.UnitLooksLikeBoss
|
|
local PickFightLabel = H.PickFightLabel
|
|
local SnapshotSegment = H.SnapshotSegment
|
|
local PushFront = H.PushFront
|
|
local MAX_RECENT_FIGHTS = H.MAX_RECENT_FIGHTS
|
|
local MAX_BOSS_FIGHTS = H.MAX_BOSS_FIGHTS
|
|
|
|
P:FlushActiveCCs()
|
|
|
|
local now = GetTime()
|
|
local startT = O.data.current.startTime or 0
|
|
duration = duration or ((startT > 0) and (now - startT) or 0)
|
|
|
|
local lastAct = O.data.current.lastActivityTime
|
|
if startT > 0 and lastAct and lastAct >= startT then
|
|
local trimmed = lastAct - startT
|
|
if trimmed > 0 and (duration <= 0 or trimmed < duration) then
|
|
duration = trimmed
|
|
end
|
|
end
|
|
if duration < 0 then duration = 0 end
|
|
O.data.current.duration = duration
|
|
-- Freeze endTime to last activity so the UI does not keep ticking
|
|
if lastAct and lastAct >= startT then
|
|
O.data.current.endTime = lastAct
|
|
else
|
|
O.data.current.endTime = now
|
|
end
|
|
|
|
if duration > 0 and O.data.current.players then
|
|
-- Never divide by sub-second durations for overall rate averages
|
|
local rateDur = duration
|
|
if rateDur < 1 then rateDur = 1 end
|
|
local name, pdata
|
|
for name, pdata in pairs(O.data.current.players) do
|
|
local o = EnsurePlayer(O.data.overall, name)
|
|
if o then
|
|
local dmg = pdata.damage or 0
|
|
local heal = pdata.healing or 0
|
|
if dmg > 0 then
|
|
o.dpsSum = (o.dpsSum or 0) + (dmg / rateDur)
|
|
o.dpsSamples = (o.dpsSamples or 0) + 1
|
|
end
|
|
if heal > 0 then
|
|
o.hpsSum = (o.hpsSum or 0) + (heal / rateDur)
|
|
o.hpsSamples = (o.hpsSamples or 0) + 1
|
|
end
|
|
end
|
|
end
|
|
O.data.overall.duration = (O.data.overall.duration or 0) + duration
|
|
if not O.data.overall.startTime or O.data.overall.startTime == 0 then
|
|
O.data.overall.startTime = startT
|
|
end
|
|
O.data.overall.endTime = now
|
|
end
|
|
|
|
-- Boss segment = any unique enemy damaged this fight that matched boss criteria
|
|
if H.ResolveBossFromDamagedEnemies then
|
|
H.ResolveBossFromDamagedEnemies(O.data.current)
|
|
else
|
|
ResolveBossFromDamagedEnemies(O.data.current)
|
|
end
|
|
-- Sync into S (OnCombatEnd uses S alias of ST)
|
|
S.fightIsBoss = ST.fightIsBoss
|
|
S.fightBossName = ST.fightBossName
|
|
|
|
if S.fightBossName and not IsUniqueEnemyName(S.fightBossName) then
|
|
S.fightIsBoss = false
|
|
S.fightBossName = nil
|
|
ST.fightIsBoss = false
|
|
ST.fightBossName = nil
|
|
end
|
|
|
|
local label = PickFightLabel()
|
|
O.data.current.label = label
|
|
O.data.current.isBoss = S.fightIsBoss and true or false
|
|
|
|
local hasData = false
|
|
for _, _ in pairs(O.data.current.players) do
|
|
hasData = true
|
|
break
|
|
end
|
|
|
|
if hasData and duration >= 0.5 then
|
|
local snap = SnapshotSegment(O.data.current)
|
|
PushFront(O.data.recentFights, snap, MAX_RECENT_FIGHTS)
|
|
if snap.isBoss then
|
|
PushFront(O.data.bossFights, snap, MAX_BOSS_FIGHTS)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Parser:OnReset()
|
|
local P = H.Parser or Parser
|
|
local O = H.OM or OM
|
|
local S = H.ST or ST
|
|
if not S then S = {} ; H.ST = S end
|
|
if P and P.FlushActiveCCs then P:FlushActiveCCs() end
|
|
S.lastHitOn = {}
|
|
if H.ClearDispelBuffers then H.ClearDispelBuffers() end
|
|
O.data.current = { players = {}, ccTargets = {}, startTime = 0, endTime = 0, label = "Current", isBoss = false, duration = 0 }
|
|
O.data.overall = { players = {}, ccTargets = {}, startTime = 0, endTime = 0, label = "Overall", isBoss = false, duration = 0 }
|
|
O.data.recentFights = {}
|
|
O.data.bossFights = {}
|
|
O.data.paused = nil
|
|
O.meterPaused = false
|
|
S.fightEnemies = {}
|
|
S.fightEnemyDeaths = {}
|
|
S.fightDuplicateNames = {}
|
|
S.fightEnemyBossLike = {}
|
|
S.fightIsBoss = false
|
|
S.fightBossName = nil
|
|
S.recentAbsorbCaster = {}
|
|
S.recentShieldByTarget = {}
|
|
S.recentAbsorbCredits = {}
|
|
S.absorbAuras = {}
|
|
end
|
|
|
|
-- Resolve a UI segment key to a data table
|
|
-- Keys: "current", "overall", "recent1", "recent2", "boss1", "boss2", "boss3"
|
|
function Parser:CreatePausedSnapshot()
|
|
if not OM.data or not OM.data.current then return end
|
|
local src = OM.data.current
|
|
local snap = SnapshotSegment(src)
|
|
-- Freeze duration at the moment of pause
|
|
local dur = 0
|
|
if src.duration and src.duration > 0 then
|
|
dur = src.duration
|
|
else
|
|
local startT = src.startTime or 0
|
|
if startT > 0 then
|
|
local last = src.lastActivityTime
|
|
if last and last > startT then
|
|
dur = last - startT
|
|
else
|
|
dur = GetTime() - startT
|
|
end
|
|
end
|
|
end
|
|
if dur < 0 then dur = 0 end
|
|
snap.duration = dur
|
|
snap.startTime = src.startTime or 0
|
|
snap.endTime = GetTime()
|
|
snap.lastActivityTime = src.lastActivityTime
|
|
snap.label = "Paused"
|
|
snap.isBoss = src.isBoss and true or false
|
|
OM.data.paused = snap
|
|
end
|
|
|
|
function Parser:GetSegment(key)
|
|
if not key or key == "current" then
|
|
return OM.data.current
|
|
elseif key == "overall" then
|
|
return OM.data.overall
|
|
elseif key == "paused" then
|
|
return OM.data.paused
|
|
end
|
|
local _, _, kind, idx = string.find(key, "^(%a+)(%d+)$")
|
|
idx = tonumber(idx)
|
|
if kind == "recent" and idx and OM.data.recentFights[idx] then
|
|
return OM.data.recentFights[idx]
|
|
end
|
|
if kind == "boss" and idx and OM.data.bossFights[idx] then
|
|
return OM.data.bossFights[idx]
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Parser:OnRosterUpdate()
|
|
playerName = UnitName("player"); ST.playerName = playerName
|
|
end
|
|
|
|
-- ============================================================
|
|
-- Register with Core
|
|
-- ============================================================
|
|
|
|
|
|
-- ============================================================
|
|
-- Cross-file exports (Lua 5.0 has no shared locals between files)
|
|
-- ============================================================
|
|
local NS = GreedMeter.ParserNS
|
|
NS.Parser = Parser
|
|
NS.H = H
|
|
NS.ST = ST
|
|
|
|
NS.ModeEnabled = ModeEnabled
|
|
NS.IsAbsorbShield = IsAbsorbShield
|
|
NS.NoteRecentAbsorbCaster = NoteRecentAbsorbCaster
|
|
NS.GetRecentAbsorbCaster = GetRecentAbsorbCaster
|
|
NS.SetAbsorbAura = SetAbsorbAura
|
|
NS.ClearAbsorbAura = ClearAbsorbAura
|
|
NS.GetAbsorbApplicator = GetAbsorbApplicator
|
|
NS.RECENT_CASTER_TIMEOUT = RECENT_CASTER_TIMEOUT
|
|
|
|
NS.SpellInSet = SpellInSet
|
|
NS.SpellLookup = SpellLookup
|
|
NS.PushCapped = PushCapped
|
|
NS.ForEachGroupUnit = ForEachGroupUnit
|
|
NS.FindUnitByName = FindUnitByName
|
|
NS.SplitOverheal = SplitOverheal
|
|
NS.MarkDuplicateName = MarkDuplicateName
|
|
NS.NoteEnemyDeath = NoteEnemyDeath
|
|
NS.NoteEnemyHit = NoteEnemyHit
|
|
NS.UnitLooksLikeBoss = UnitLooksLikeBoss
|
|
NS.GetGroupAverageMaxHP = GetGroupAverageMaxHP
|
|
NS.EmptyOutcomeBucket = EmptyOutcomeBucket
|
|
NS.NoteSpellOutcome = NoteSpellOutcome
|
|
NS.NoteLastHit = NoteLastHit
|
|
|
|
NS.NormalizeName = NormalizeName
|
|
NS.EnsurePlayer = EnsurePlayer
|
|
NS.ResolveSource = ResolveSource
|
|
NS.IsTracked = IsTracked
|
|
NS.SuperWoWAvailable = SuperWoWAvailable
|
|
NS.CacheGuid = CacheGuid
|
|
NS.StripAndCacheGuids = StripAndCacheGuids
|
|
NS.RefreshGuidCacheFromUnits = RefreshGuidCacheFromUnits
|
|
NS.NoteActivity = NoteActivity
|
|
NS.DeepCopyPlayerData = DeepCopyPlayerData
|
|
NS.SnapshotSegment = SnapshotSegment
|
|
NS.PickFightLabel = PickFightLabel
|
|
NS.PushFront = PushFront
|
|
NS.ClearDispelBuffers = ClearDispelBuffers
|
|
NS.IsUniqueEnemyName = IsUniqueEnemyName
|
|
NS.guidToName = guidToName
|
|
NS.nameToGuid = nameToGuid
|
|
|
|
function Parser:CacheGuid(name, guid)
|
|
CacheGuid(name, guid)
|
|
end
|
|
|
|
function Parser:NameFromGuid(guid)
|
|
if not guid or guid == "" then return nil end
|
|
return guidToName[guid]
|
|
end
|
|
|
|
OM:RegisterModule("Parser", Parser)
|
|
|