You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

656 lines
20 KiB

--todo:
--implement divisors in the timeline (vertical lines)
do
local Details = Details
if (not Details) then
print ("Details! Not Found.")
return
end
local _
local DF = _G.DetailsFramework
local UnitGUID = _G.UnitGUID
local GetTime = _G.GetTime
local tremove = _G.tremove
local tinsert = _G.tinsert
local UnitBuff = _G.UnitBuff
local UnitIsPlayer = _G.UnitIsPlayer
local UnitInParty = _G.UnitInParty
local UnitInRaid = _G.UnitInRaid
local UnitIsUnit = _G.UnitIsUnit
local abs = _G.abs
local wipe = _G.wipe
local unpack = _G.unpack
local GetSpellInfo = Details.GetSpellInfo
--> minimal details version required to run this plugin
local MINIMAL_DETAILS_VERSION_REQUIRED = 136
local DETAILS_ATTRIBUTE_DAMAGE = _G.DETAILS_ATTRIBUTE_DAMAGE
local CASTHISTORY_VERSION = "v1.0.0"
--> create a plugin object
local castHistory = Details:NewPluginObject ("Details_CastHistory", _G.DETAILSPLUGIN_ALWAYSENABLED)
--> just localizing here the plugin's main frame
local frame = castHistory.Frame
--> set the description
castHistory:SetPluginDescription ("Show a time line of casts of players")
--> get the framework object
local framework = castHistory:GetFramework()
--> spells cache
local spellCache = {}
local auraCache = {}
--> when receiving an event from details, handle it here
local handle_details_event = function (event, ...)
if (event == "COMBAT_PLAYER_ENTER") then
castHistory.OnCombatStart (...)
elseif (event == "COMBAT_PLAYER_LEAVE") then
castHistory.OnCombatEnd()
elseif (event == "PLUGIN_DISABLED") then
--> plugin has been disabled at the details options panel
elseif (event == "PLUGIN_ENABLED") then
--> plugin has been enabled at the details options panel
elseif (event == "DETAILS_DATA_SEGMENTREMOVED") then
--> old segment got deleted by the segment limit
elseif (event == "DETAILS_DATA_RESET") then
--> combat data got wiped
end
end
function castHistory.InstallTab()
local tabName = "CastTimeline"
local tabNameLoc = "Cast Timeline (New Plugin Pls Test)"
local canShowTab = function (tabOBject, playerObject)
local combat = Details:GetCombatFromBreakdownWindow()
if (combat) then
if (combat.spells_cast_timeline [playerObject.serial]) then
tabOBject.frame:Show()
return true
end
end
return false
end
local fillTab = function (tab, playerObject, combat)
local castData = combat.spells_cast_timeline [playerObject and playerObject.serial]
if (castData and castData[1]) then
local spellData = {}
local firstAbilityUsedTime = castData[1][1] --gettime
local combatTime = combat:GetCombatTime() --seconds elapsed
local combatStartTime = combat:GetStartTime() --gettime
local combatEndTime = combat:GetEndTime() --gettime
local auraData = combat.aura_timeline [playerObject and playerObject.serial]
--cast data
for i = 1, #castData do
--cast: [1] GetTime(), [2] spellId, [3] target
local cast = castData [i]
local atTime = cast [1] --when it was casted
local spellId = cast [2]
local payload = cast [3] --contains the spell target
local auraTimers = auraData[spellId]
if (auraTimers) then
auraTimers = auraTimers.data
local foundAuraTimer = false
for o = 1, #auraTimers do
local auraTimer = auraTimers[o]
--print (select(1, GetSpellInfo(spellId)), atTime, auraTimer[1])
if (DF:IsNearlyEqual(auraTimer[1], atTime, 0.650)) then --when it was applyed, using 650ms as latency compensation
foundAuraTimer = true
local auraActivationData = auraTimer
local applied = auraActivationData [1]
local wentOff = auraActivationData [2]
local duration = wentOff - applied
spellData [spellId] = spellData [spellId] or {}
tinsert (spellData[spellId], {applied - combatStartTime, 10, duration, duration, ["payload"] = payload})
break
end
end
if (not foundAuraTimer) then
spellData [spellId] = spellData [spellId] or {}
tinsert (spellData[spellId], {atTime - combatStartTime, 1, ["payload"] = payload})
end
else
spellData [spellId] = spellData [spellId] or {}
tinsert (spellData[spellId], {atTime - combatStartTime, 1, ["payload"] = payload})
end
end
local scrollData = {
length = combatTime,
defaultColor = {1, 1, 1, 1},
useIconOnBlocks = true,
lines = {},
}
for spellId, spellTimers in pairs (spellData) do
local spellName, _, spellIcon = GetSpellInfo(spellId)
local t = {
text = spellName,
icon = spellIcon,
spellId = spellId,
timeline = spellTimers, --each table inside has the .payload
}
tinsert (scrollData.lines, t)
end
tab.frame.scroll:SetData (scrollData)
end
end
local onClickTab = function()
--tab.frame:Show()
end
local onCreatedTab = function (tab, frame)
local GameCooltip2 = _G.GameCooltip2
local scroll = DF:CreateTimeLineFrame (frame, "$parentTimeLine",
{ --general options
width = 865, height = 438,
backdrop_color = {1, 1, 1, .1},
backdrop_color_highlight = {1, 1, 1, .3},
slider_backdrop_color = {.2, .2, .2, 0.3},
slider_backdrop_border_color = {0.2, 0.2, 0.2, .3},
on_enter = function (self)
self:SetBackdropColor (unpack (self.backdrop_color_highlight))
end,
on_leave = function (self)
if (self.dataIndex % 2 == 1) then
self:SetBackdropColor (1, 1, 1, 0)
else
self:SetBackdropColor (1, 1, 1, .1)
end
end,
block_on_enter = function (self)
local info = self.info
local spellName, _, spellIcon = GetSpellInfo(info.spellId)
GameCooltip2:Preset (2)
GameCooltip2:SetOwner (self)
GameCooltip2:SetOption ("TextSize", 10)
GameCooltip2:AddLine (spellName)
GameCooltip2:AddIcon (spellIcon, 1, 1, 16, 16, .1, .9, .1, .9)
GameCooltip2:AddLine ("Time:", info.time < -0.2 and "-" .. DF:IntegerToTimer(abs(info.time)) or DF:IntegerToTimer(abs(info.time)))
if (info.duration) then
GameCooltip2:AddLine ("Duration:", DF:IntegerToTimer (info.duration))
end
local target = info.payload
GameCooltip2:AddLine("Target:", info.payload)
GameCooltip2:AddLine("SpellId:", info.spellId)
GameCooltip2:Show()
self.icon:SetBlendMode ("ADD")
end,
block_on_leave = function (self)
GameCooltip2:Hide()
self.icon:SetBlendMode ("BLEND")
end,
},
{ --timeline frame options
draw_line_color = {1, 1, 1, 0.03},
}
)
local scrollText = DF:CreateLabel (scroll.horizontalSlider, "scroll", 10, {.5, .5, .5, .5})
scrollText:SetPoint ("right", scroll.horizontalSlider, "right", -2, 0)
local zoomText = DF:CreateLabel (scroll.scaleSlider, "zoom", 10, {.5, .5, .5, .5})
zoomText:SetPoint ("right", scroll.scaleSlider, "right", -2, 0)
scroll:SetPoint ("topleft", frame, "topleft", 0, 0)
frame.scroll = scroll
end
local iconSettings = {
texture = [[Interface\CHATFRAME\CHATFRAMEBACKGROUND]],
coords = {0.1, 0.9, 0.1, 0.9},
width = 16,
height = 16,
}
Details:CreatePlayerDetailsTab (tabName, tabNameLoc, canShowTab, fillTab, nil, onCreatedTab, iconSettings)
end
--runs on each member in the group
function castHistory.BuildPlayerList(unitId, combatObject)
local GUID = UnitGUID (unitId)
combatObject.spells_cast_timeline [GUID] = {}
combatObject.aura_timeline [GUID] = {}
end
--runs on each member in the group
--these are stored spells when out of combat to show pre-casts before a pull
function castHistory.CacheTransfer(unitId, combatObject, spellCache, auraCache)
local GUID = UnitGUID(unitId)
local cachedCastTable = spellCache [GUID]
local combatCastTable = combatObject.spells_cast_timeline [GUID]
if (cachedCastTable and combatCastTable) then
local cutOffTime = GetTime() - 5
for i = #cachedCastTable, 1, -1 do
if (cachedCastTable [i][1] < cutOffTime) then
tremove(cachedCastTable, i)
end
end
for i = 1, #cachedCastTable do
tinsert(combatCastTable, cachedCastTable[i])
end
table.wipe(cachedCastTable)
end
--
local cachedAuraTable = auraCache [GUID]
local combatAuraTable = combatObject.aura_timeline [GUID]
if (cachedAuraTable and combatAuraTable) then
for spellId, appliedAt in pairs (cachedAuraTable) do
--check if the aura is still up
local auraUp
for i = 1, 40 do
local name, texture, count, debuffType, duration, expirationTime, caster, canStealOrPurge, nameplateShowPersonal, auraSpellId = UnitBuff(unitId, i)
if (not name) then
break
end
if (spellId == auraSpellId) then
auraUp = true
break
end
end
if (auraUp) then
combatAuraTable [spellId] = {
enabled = true,
appliedAt = appliedAt,
data = {},
spellId = spellId,
auraType = "BUFF",
}
end
end
table.wipe(cachedAuraTable)
end
end
function castHistory.OnCombatStart (combatObject)
--start
castHistory.inCombat = true
castHistory.currentCombat = combatObject
--built the table
DF:GroupIterator (castHistory.BuildPlayerList, combatObject)
--precasts
DF:GroupIterator (castHistory.CacheTransfer, combatObject, spellCache, auraCache)
--wipe sent cache
wipe(castHistory.sentSpellCache)
castHistory.combatLogReader:RegisterEvent ("COMBAT_LOG_EVENT_UNFILTERED")
end
function castHistory.OnCombatEnd()
--finish
castHistory.inCombat = false
--close aura timers
if (castHistory.currentCombat) then
for GUID, playerAuraTable in pairs(castHistory.currentCombat.aura_timeline) do
for spellId, auraTable in pairs(playerAuraTable) do
if (auraTable.enabled) then
auraTable.enabled = false
auraTable.amountUp = 0
tinsert (auraTable.data, {auraTable.appliedAt, GetTime()}) --when it was applied and when it went off
end
end
end
end
wipe(castHistory.sentSpellCache)
castHistory.combatLogReader:UnregisterEvent ("COMBAT_LOG_EVENT_UNFILTERED")
end
function castHistory:OnEvent (_, event, ...)
if (event == "UNIT_AURA disabled") then
local unitId = ...
if (unitId and UnitIsPlayer(unitId) and (UnitInParty(unitId) or UnitInRaid(unitId) or UnitIsUnit(unitId, "player"))) then
local GUID = UnitGUID(unitId)
if (GUID) then
if (castHistory.inCombat) then
local playerAuraTable = castHistory.currentCombat.aura_timeline [GUID]
if (playerAuraTable) then
local aurasFound = {}
for i = 1, 40 do
local name, texture, count, debuffType, duration, expirationTime, caster, canStealOrPurge, nameplateShowPersonal, spellId = UnitBuff(unitId, i)
if (not name) then
break
end
if (duration and duration > 0 and duration < 50) then
--check if can merge
spellId = castHistory.db.merged_spells[spellId] or spellId
local auraTable = playerAuraTable [spellId]
if (not auraTable) then
playerAuraTable [spellId] = {
enabled = true,
appliedAt = GetTime(),
data = {},
spellId = spellId,
auraType = "BUFF",
}
auraTable = playerAuraTable [spellId]
else
if (not auraTable.enabled) then
auraTable.enabled = true
auraTable.appliedAt = GetTime()
end
end
aurasFound [spellId] = true
end
end
for spellId, auraTable in pairs (playerAuraTable) do
if (auraTable.enabled and not aurasFound [spellId]) then
auraTable.enabled = false
tinsert (auraTable.data, {auraTable.appliedAt, GetTime()}) --when it was applied and when it went off
end
end
end
else
--details! isn't in combat
--add the spell into the cache for pre-cast spells
local cachedAuraTable = auraCache [GUID] or {}
auraCache [GUID] = cachedAuraTable
for i = 1, 40 do
local name, texture, count, debuffType, duration, expirationTime, caster, canStealOrPurge, nameplateShowPersonal, spellId = UnitBuff (unitId, i)
if (not name) then
break
end
--check if can merge
spellId = castHistory.db.merged_spells[spellId] or spellId
if (duration and duration > 0 and duration < 50) then
cachedAuraTable [spellId] = expirationTime - duration
end
end
end
end
end
elseif (event == "UNIT_SPELLCAST_SENT") then
local unitId, target, castGUID, spellId = ...
local spell = GetSpellInfo(spellId)
if (unitId == "player") then
castHistory.sentSpellCache[castGUID] = {
target = target or "",
caster = unitId,
spellId = spellId,
}
end
--print("sent", castHistory.sentSpellCache[castGUID])
elseif (event == "UNIT_SPELLCAST_SUCCEEDED") then
local unitId, castGUID, spellId = ...
local castSent = castHistory.sentSpellCache[castGUID]
if (not castSent) then
return
end
castHistory.sentSpellCache [castGUID] = nil
local target = castSent.target
local caster = castSent.caster
--local spellId = castSent.spellId
if (caster and spellId and UnitIsPlayer(caster) and (UnitInParty(caster) or UnitInRaid(caster) or UnitIsUnit(caster, "player"))) then
local GUID = UnitGUID(caster)
if (GUID) then
--check if can merge
spellId = castHistory.db.merged_spells[spellId] or spellId
if (castHistory.inCombat) then
local playerCastTable = castHistory.currentCombat.spells_cast_timeline [GUID]
if (playerCastTable) then
tinsert(playerCastTable, {GetTime(), spellId, target})
end
else
local playerCastTable = spellCache [GUID]
if (not playerCastTable) then
spellCache [GUID] = {}
playerCastTable = spellCache [GUID]
else
local cutOffTime = GetTime() - 5
for i = #playerCastTable, 1, -1 do
if (playerCastTable [i][1] < cutOffTime) then
tremove (playerCastTable, i)
end
end
end
tinsert(playerCastTable, {GetTime(), spellId, target})
end
end
end
elseif (event == "ADDON_LOADED") then
local AddonName = select (1, ...)
if (AddonName == "Details_CastHistory") then
--> every plugin must have a OnDetailsEvent function
function castHistory:OnDetailsEvent (event, ...)
return handle_details_event (event, ...)
end
local default_options = {
ignored_spells = {
[324988] = true, --shared suffering (lady inerva darkvein)
[324183] = true, --lightless force
--hunter
[75] = true, --auto shot
--warrior
[184367] = 201363, --rampage
[184709] = 201363,
[199667] = 44949, --whirlwind
[190411] = 44949,
[85739] = 44949,
[317488] = 317485, --condemn
[126664] = 100, --charge
[227847] = 50622, --bladestorm
[147833] = 3411, --intervene
[316531] = 3411, --intervene
[52174] = 6544, --heroic leap
--prist
[33076] = 41635, --prayer of mending
--rogue
[199672] = 1943, --rupture
[185313] = 185422, --shadow dance
--mage
[7268] = 5143, --arcane missiles
[228597] = true, --frostbolt
--warlock
[146739] = 172, --corruption
--monk
[148187] = 116847, --rushing jade wing
--dh
[222031] = 199547, --chaos strike
[201453] = 191427, --metamorphosis
[201428] = true, --annihilation
[227518] = true, --annihilation
--dk
[196711] = true, --remorseless winter
},
merged_spells = {
},
}
--> Install: install -> if successful installed; saveddata -> a table saved inside details db, used to save small amount of data like configs
local install, saveddata = Details:InstallPlugin("RAID",
"Cast History",
"Interface\\Icons\\Ability_Warrior_BattleShout",
castHistory,
"DETAILS_PLUGIN_CAST_HISTORY",
MINIMAL_DETAILS_VERSION_REQUIRED,
"Terciob",
CASTHISTORY_VERSION,
default_options)
if (type (install) == "table" and install.error) then
print (install.error)
end
--> registering details events we need
Details:RegisterEvent (castHistory, "COMBAT_PLAYER_ENTER") --when details creates a new segment, not necessary the player entering in combat.
Details:RegisterEvent (castHistory, "COMBAT_PLAYER_LEAVE") --when details finishs a segment, not necessary the player leaving the combat.
Details:RegisterEvent (castHistory, "DETAILS_DATA_RESET") --details combat data has been wiped
castHistory.Frame:RegisterEvent ("UNIT_SPELLCAST_SENT")
castHistory.Frame:RegisterEvent ("UNIT_SPELLCAST_SUCCEEDED")
castHistory.Frame:RegisterEvent ("UNIT_AURA")
castHistory.sentSpellCache = {}
castHistory.InstallTab()
--combatlog parser
local combatLogReader = CreateFrame("frame")
local CombatLogGetCurrentEventInfo = _G.CombatLogGetCurrentEventInfo
local playerSerial = UnitGUID("player")
castHistory.combatLogReader = combatLogReader
local eventFunc = {
["SPELL_AURA_APPLIED"] = function(timew, token, hidding, sourceSerial, sourceName, sourceFlag, sourceFlag2, targetSerial, targetName, targetFlag, targetFlag2, spellId, spellName, spellType, amount, overKill, school, resisted, blocked, absorbed, isCritical)
local GUID = sourceSerial
if (castHistory.inCombat) then
local playerAuraTable = castHistory.currentCombat.aura_timeline[GUID]
if (playerAuraTable) then
spellId = castHistory.db.merged_spells[spellId] or spellId
local auraTable = playerAuraTable[spellId]
if (not auraTable) then
playerAuraTable [spellId] = {
enabled = true,
amountUp = 1,
appliedAt = GetTime(),
data = {},
spellId = spellId,
--auraType = "BUFF",
}
auraTable = playerAuraTable [spellId]
else
if (not auraTable.enabled) then
auraTable.enabled = true
auraTable.amountUp = 1
auraTable.appliedAt = GetTime()
else
auraTable.amountUp = auraTable.amountUp + 1
end
end
end
else
--not in combat
end
end,
["SPELL_AURA_REMOVED"] = function(timew, token, hidding, sourceSerial, sourceName, sourceFlag, sourceFlag2, targetSerial, targetName, targetFlag, targetFlag2, spellId, spellName, spellType, amount, overKill, school, resisted, blocked, absorbed, isCritical)
local GUID = sourceSerial
if (castHistory.inCombat) then
local playerAuraTable = castHistory.currentCombat.aura_timeline[GUID]
if (playerAuraTable) then
spellId = castHistory.db.merged_spells[spellId] or spellId
local auraTable = playerAuraTable[spellId]
if (auraTable) then
if (auraTable.enabled) then
auraTable.amountUp = auraTable.amountUp - 1
if (auraTable.amountUp <= 0) then
auraTable.enabled = false
tinsert(auraTable.data, {auraTable.appliedAt, GetTime()}) --when it was applied and when it went off
end
end
end
end
end
end,
}
combatLogReader:SetScript ("OnEvent", function (self)
local timew, token, hidding, sourceSerial, sourceName, sourceFlag, sourceFlag2, targetSerial, targetName, targetFlag, targetFlag2, spellId, spellName, spellType, amount, overKill, school, resisted, blocked, absorbed, isCritical = CombatLogGetCurrentEventInfo()
local func = eventFunc[token]
if (func) then
--check if the event is from the player or from the group
if (sourceName and spellId and (UnitInParty(sourceName) or UnitInRaid(sourceName) or UnitIsUnit(sourceName, "player"))) then
func(timew, token, hidding, sourceSerial, sourceName, sourceFlag, sourceFlag2, targetSerial, targetName, targetFlag, targetFlag2, spellId, spellName, spellType, amount, overKill, school, resisted, blocked, absorbed, isCritical)
end
end
end)
end
end
end
end