--[[-------------------------------------------------------------------- Copyright (C) 2012, 2013 Sidoine De Wispelaere. Copyright (C) 2012, 2013, 2014 Johnny C. Lam. See the file LICENSE.txt for copying permission. --]]-------------------------------------------------------------------- -- The travelling missiles or spells that have been cast but whose effects were not still not applied local OVALE, Ovale = ... local OvaleFuture = Ovale:NewModule("OvaleFuture", "AceEvent-3.0") Ovale.OvaleFuture = OvaleFuture --<private-static-properties> local OvalePool = Ovale.OvalePool local OvaleProfiler = Ovale.OvaleProfiler -- Forward declarations for module dependencies. local OvaleAura = nil local OvaleCooldown = nil local OvaleData = nil local OvaleGUID = nil local OvalePaperDoll = nil local OvaleScore = nil local OvaleSpellBook = nil local OvaleState = nil local gmatch = string.gmatch local ipairs = ipairs local next = next local pairs = pairs local strfind = string.find local tinsert = table.insert local tonumber = tonumber local tostring = tostring local tremove = table.remove local type = type local wipe = wipe local API_GetTime = GetTime local API_UnitCastingInfo = UnitCastingInfo local API_UnitChannelInfo = UnitChannelInfo local API_UnitGUID = UnitGUID local API_UnitName = UnitName local MAX_COMBO_POINTS = MAX_COMBO_POINTS -- Register for profiling. OvaleProfiler:RegisterProfiling(OvaleFuture) -- Player's GUID. local self_guid = nil -- The spells that the player is casting or has cast but are still in-flight toward their targets. local self_activeSpellcast = {} -- self_lastSpellcast[targetGUID][spellId] is the most recent spell that has landed successfully on the target. local self_lastSpellcast = {} local self_pool = OvalePool("OvaleFuture_pool") do self_pool.Clean = function(self, spellcast) -- Release reference-counted snapshot before wiping. if spellcast.snapshot then OvalePaperDoll:ReleaseSnapshot(spellcast.snapshot) end end end -- Used to track the most recent spellcast started with UNIT_SPELLCAST_SENT. local self_lastLineID = nil local self_lastSpell = nil local self_lastTarget = nil -- Time at which a player aura was last added. local self_timeAuraAdded = nil -- Table of external functions to save additional data about a spellcast. local self_updateSpellcastInfo = {} local OVALE_UNKNOWN_GUID = 0 -- These CLEU events are eventually received after a successful spellcast. local CLEU_AURA_EVENT = { SPELL_AURA_APPLIED = "hit", SPELL_AURA_APPLIED_DOSE = "hit", SPELL_AURA_BROKEN = "hit", SPELL_AURA_BROKEN_SPELL = "hit", SPELL_AURA_REFRESH = "hit", SPELL_AURA_REMOVED = "hit", SPELL_AURA_REMOVED_DOSE = "hit", } local CLEU_SUCCESSFUL_SPELLCAST_EVENT = { -- SPELL_CAST_SUCCESS = "hit", SPELL_CAST_FAILED = "miss", SPELL_DAMAGE = "hit", SPELL_DISPEL = "hit", SPELL_DISPEL_FAILED = "miss", SPELL_HEAL = "hit", SPELL_INTERRUPT = "hit", SPELL_MISSED = "miss", SPELL_STOLEN = "hit", } do -- All aura events are also successful spellcast events. for cleuEvent, v in pairs(CLEU_AURA_EVENT) do CLEU_SUCCESSFUL_SPELLCAST_EVENT[cleuEvent] = v end end -- Table of spells that are "white attacks" but are also tracked in UNIT_SPELLCAST_* events. local WHITE_ATTACK = { [ 75] = true, -- Auto Shot [ 5019] = true, -- Shoot } --[[ This is the delta added to the starting cast time of the spell in the simulator. This ensures that the time in the simulator is just after the spell has started being cast. --]] local SIMULATOR_LAG = 0.005 --</private-static-properties> --<public-static-properties> -- Whether the player is in combat. OvaleFuture.inCombat = nil -- The time that combat started. OvaleFuture.combatStartTime = nil -- Spell counter (see Counter script condition). OvaleFuture.counter = {} -- Most recent spellcast. OvaleFuture.lastSpellcast = nil -- Most recent GCD spellcast. OvaleFuture.lastGCDSpellcast = nil -- Table of most recent cast times of spells, indexed by spell ID. OvaleFuture.lastCastTime = {} -- Debugging: spells to trace OvaleFuture.traceSpellList = nil --</public-static-properties> --<private-static-methods> local function TracePrintf(spellId, ...) if OvaleFuture.traceSpellList then local name = spellId if type(spellId) == "number" then name = OvaleSpellBook:GetSpellName(spellId) end if OvaleFuture.traceSpellList[spellId] or OvaleFuture.traceSpellList[name] then local now = API_GetTime() OvaleFuture:Print("[trace] @%f %s", now, Ovale:MakeString(...)) end end end --[[ Return the spell-specific damage multiplier using the information from SpellDamage{Buff,Debuff} declarations. This doesn't include the base damage multiplier of the character kept in snapshots. auraObject is an object that provides the following two methods: GetAura(unitId, auraId, filter, mine) IsActiveAura(aura, atTime) --]] local function GetDamageMultiplier(spellId, snapshot, auraObject) auraObject = auraObject or OvaleAura local damageMultiplier = 1 local si = OvaleData.spellInfo[spellId] if si and si.aura and si.aura.damage then for filter, auraList in pairs(si.aura.damage) do for auraId, spellData in pairs(auraList) do local tokenIterator, multiplier if strfind(spellData, ",") then tokenIterator = gmatch(spellData, "[^,]+") multiplier = tokenIterator() else multiplier = spellData end multiplier = tonumber(multiplier) local verified if tokenIterator then if auraObject.CheckRequirements then verified = auraObject.CheckRequirements(auraObject, spellId, tokenIterator, "player") else verified = OvaleData:CheckRequirements(spellId, tokenIterator, "player") end else verified = true end if verified then local aura = auraObject:GetAura("player", auraId, filter) if auraObject:IsActiveAura(aura) then local siAura = OvaleData.spellInfo[auraId] -- If an aura does stacking damage, then it needs to set stacking=1. if siAura and siAura.stacking and siAura.stacking > 0 then multiplier = 1 + (multiplier - 1) * aura.stacks end damageMultiplier = damageMultiplier * multiplier end end end end end -- Factor in additional damage multipliers that are registered with this module. for tbl in pairs(self_updateSpellcastInfo) do if tbl.GetDamageMultiplier then local multiplier = tbl.GetDamageMultiplier(spellId, snapshot, auraObject) damageMultiplier = damageMultiplier * multiplier end end return damageMultiplier end local function QueueSpellcast(spellId, lineId, startTime, endTime, channeled, allowRemove) OvaleFuture:StartProfiling("OvaleFuture_QueueSpellcast") local self = OvaleFuture local spellcast = self_pool:Get() spellcast.spellId = spellId spellcast.lineId = lineId spellcast.start = startTime spellcast.stop = endTime spellcast.channeled = channeled spellcast.allowRemove = allowRemove -- Set the target from the data taken from UNIT_SPELLCAST_SENT if it's the same spellcast. local spellName = OvaleSpellBook:GetSpellName(spellId) if lineId == self_lastLineID and spellName == self_lastSpell then spellcast.target = self_lastTarget else spellcast.target = API_UnitGUID("target") end local target = OvaleGUID:GetUnitId(spellcast.target) TracePrintf(spellId, " QueueSpellcast: %s (%d), lineId=%d, startTime=%f, endTime=%f, target=%s (%s)", spellName, spellId, lineId, startTime, endTime, spellcast.target, target) -- Snapshot the current stats for the spellcast. spellcast.snapshot = OvalePaperDoll:GetSnapshot() spellcast.damageMultiplier = GetDamageMultiplier(spellId, spellcast.snapshot) local si = OvaleData.spellInfo[spellId] if si then -- Save additional information to the spellcast that are registered with this module. for tbl in pairs(self_updateSpellcastInfo) do if tbl.SaveToSpellcast then tbl.SaveToSpellcast(spellcast) end end -- Track one of the auras, if any, that are added or refreshed by this spell. -- This helps to later identify whether the spellcast succeeded by noting when -- the aura is applied or refreshed. if si.aura then -- Look for target auras before player auras applied by the spell. if not spellcast.auraId and si.aura.target then for filter, auraList in pairs(si.aura.target) do for auraId, spellData in pairs(auraList) do local tokenIterator, value if strfind(spellData, ",") then tokenIterator = gmatch(spellData, "[^,]+") value = tokenIterator() else value = spellData end if value == "extend" then -- Advance past the number of seconds to extend the aura. local seconds = tokenIterator and tokenIterator() or nil if not seconds then Ovale:OneTimeMessage("Warning: '%d=%s' has '%s' missing duration.", auraId, spellData, value) end else local asNumber = tonumber(value) value = asNumber or value end local verified if tokenIterator then verified = OvaleData:CheckRequirements(spellId, tokenIterator, target) else verified = true end if verified and (type(value) == "string" or type(value) == "number" and value > 0) then spellcast.auraId = auraId if target ~= "player" then spellcast.removeOnAuraSuccess = true end break end end end end if not spellcast.auraId and si.aura.player then for filter, auraList in pairs(si.aura.player) do for auraId, spellData in pairs(auraList) do local tokenIterator = gmatch(spellData, "[^,]+") local value = tokenIterator() if value == "extend" then -- Skip the number of seconds to extend the aura. tokenIterator() end local verified = OvaleData:CheckRequirements(spellId, tokenIterator, target) if verified and (type(value) == "string" or type(value) == "number" and value > 0) then spellcast.auraId = auraId break end end end end end -- Increase or reset any counters used by the Counter() condition. if si.resetcounter then local resetcounter = OvaleData:GetSpellInfoProperty(spellId, "resetcounter", target) if resetcounter then self.counter[resetcounter] = 0 end end if si.inccounter then local inccounter = OvaleData:GetSpellInfoProperty(spellId, "inccounter", target) if inccounter then local oldValue = self.counter[inccounter] or 0 self.counter[inccounter] = oldValue + 1 end end end -- Set the condition for detecting a successful spellcast. if not spellcast.removeOnAuraSuccess then spellcast.removeOnSuccess = true end tinsert(self_activeSpellcast, spellcast) OvaleScore:ScoreSpell(spellId) Ovale.refreshNeeded["player"] = true OvaleFuture:StopProfiling("OvaleFuture_QueueSpellcast") return spellcast end local function UnqueueSpellcast(spellId, lineId) OvaleFuture:StartProfiling("OvaleFuture_UnqueueSpellcast") for index, spellcast in ipairs(self_activeSpellcast) do if spellcast.lineId == lineId then TracePrintf(spellId, " UnqueueSpellcast: %s (%d)", OvaleSpellBook:GetSpellName(spellId), spellId) tremove(self_activeSpellcast, index) self_pool:Release(spellcast) break end end Ovale.refreshNeeded["player"] = true OvaleFuture:StopProfiling("OvaleFuture_UnqueueSpellcast") end -- UpdateLastSpellcast() is called at the end of the event handler for CLEU_SUCCESSFUL_SPELLCAST_EVENT[]. -- It saves the given spellcast as the most recent one on its target and ensures that the spellcast -- snapshot values are correctly adjusted for buffs that are added or cleared simultaneously with the -- spellcast. local function UpdateLastSpellcast(spellcast) OvaleFuture:StartProfiling("OvaleFuture_UpdateLastSpellcast") local self = OvaleFuture local targetGUID = spellcast.target local spellId = spellcast.spellId if targetGUID and spellId then if not self_lastSpellcast[targetGUID] then self_lastSpellcast[targetGUID] = {} end local oldSpellcast = self_lastSpellcast[targetGUID][spellId] if oldSpellcast then self_pool:Release(oldSpellcast) end self_lastSpellcast[targetGUID][spellId] = spellcast self.lastSpellcast = spellcast local unitId = OvaleGUID:GetUnitId(targetGUID) local gcd = OvaleData:GetSpellInfoProperty(spellId, "gcd", unitId) if not gcd or gcd > 0 then self.lastGCDSpellcast = spellcast end --[[ If any auras have been added between the start of the spellcast and this event, then take a more recent snapshot of the player stats for this spellcast. This is needed to see any auras that were applied at the same time as the spellcast, e.g., potions or other on-use abilities or items. --]] if self_timeAuraAdded then if self_timeAuraAdded >= spellcast.start and self_timeAuraAdded - spellcast.stop < 1 then OvalePaperDoll:UpdateSnapshot(spellcast.snapshot) spellcast.damageMultiplier = GetDamageMultiplier(spellId, spellcast.snapshot) TracePrintf(spellId, " Updated spell info for %s (%d) to snapshot from %f.", OvaleSpellBook:GetSpellName(spellId), spellId, spellcast.snapshot.snapshotTime) end end end OvaleFuture:StopProfiling("OvaleFuture_UpdateLastSpellcast") end --</private-static-methods> --<public-static-methods> function OvaleFuture:OnInitialize() -- Resolve module dependencies. OvaleAura = Ovale.OvaleAura OvaleCooldown = Ovale.OvaleCooldown OvaleData = Ovale.OvaleData OvaleGUID = Ovale.OvaleGUID OvalePaperDoll = Ovale.OvalePaperDoll OvaleScore = Ovale.OvaleScore OvaleSpellBook = Ovale.OvaleSpellBook OvaleState = Ovale.OvaleState end function OvaleFuture:OnEnable() self_guid = API_UnitGUID("player") self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") self:RegisterEvent("PLAYER_ENTERING_WORLD") self:RegisterEvent("PLAYER_REGEN_DISABLED") self:RegisterEvent("PLAYER_REGEN_ENABLED") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP") self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") self:RegisterEvent("UNIT_SPELLCAST_SENT") self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") self:RegisterEvent("UNIT_SPELLCAST_START") self:RegisterMessage("Ovale_AuraAdded") self:RegisterMessage("Ovale_InactiveUnit") OvaleState:RegisterState(self, self.statePrototype) end function OvaleFuture:OnDisable() OvaleState:UnregisterState(self) self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED") self:UnregisterEvent("PLAYER_ENTERING_WORLD") self:UnregisterEvent("PLAYER_REGEN_DISABLED") self:UnregisterEvent("PLAYER_REGEN_ENABLED") self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START") self:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP") self:UnregisterEvent("UNIT_SPELLCAST_INTERRUPTED") self:UnregisterEvent("UNIT_SPELLCAST_SENT") self:UnregisterEvent("UNIT_SPELLCAST_START") self:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED") self:UnregisterMessage("Ovale_AuraAdded") self:UnregisterMessage("Ovale_InactiveUnit") self:PLAYER_ENTERING_WORLD("OnDisable") self_pool:Drain() end function OvaleFuture:PLAYER_ENTERING_WORLD(event) -- Empty out self_lastSpellcast. for guid in pairs(self_lastSpellcast) do self:Ovale_InactiveUnit(event, guid) end wipe(self.lastCastTime) end function OvaleFuture:PLAYER_REGEN_DISABLED(event) local now = API_GetTime() self.inCombat = true self.combatStartTime = now self:SendMessage("Ovale_CombatStarted", now) end function OvaleFuture:PLAYER_REGEN_ENABLED(event) local now = API_GetTime() self.inCombat = false self_pool:Drain() self:SendMessage("Ovale_CombatEnded", now) end function OvaleFuture:Ovale_AuraAdded(event, timestamp, guid, spellId, caster) if guid == self_guid then self_timeAuraAdded = timestamp end end function OvaleFuture:Ovale_InactiveUnit(event, guid) -- Remove spellcasts for inactive units. local spellTable = self_lastSpellcast[guid] if spellTable then for spellId, spellcast in pairs(spellTable) do spellTable[spellId] = nil self_pool:Release(spellcast) end self_lastSpellcast[guid] = nil end end function OvaleFuture:UNIT_SPELLCAST_CHANNEL_START(event, unit, name, rank, lineId, spellId) if unit == "player" then local _, _, _, _, startTime, endTime = API_UnitChannelInfo("player") TracePrintf(spellId, "%s: %d, lineId=%d, startTime=%f, endTime=%f", event, spellId, lineId, startTime, endTime) QueueSpellcast(spellId, lineId, startTime/1000, endTime/1000, true, false) end end function OvaleFuture:UNIT_SPELLCAST_CHANNEL_STOP(event, unit, name, rank, lineId, spellId) if unit == "player" then TracePrintf(spellId, "%s: %d, lineId=%d", event, spellId, lineId) UnqueueSpellcast(spellId, lineId) end end --Called when a spell started its cast function OvaleFuture:UNIT_SPELLCAST_START(event, unit, name, rank, lineId, spellId) if unit == "player" then local _, _, _, _, startTime, endTime = API_UnitCastingInfo("player") TracePrintf(spellId, "%s: %d, lineId=%d, startTime=%f, endTime=%f", event, spellId, lineId, startTime, endTime) QueueSpellcast(spellId, lineId, startTime/1000, endTime/1000, false, false) end end --Called if the player interrupted early his cast function OvaleFuture:UNIT_SPELLCAST_INTERRUPTED(event, unit, name, rank, lineId, spellId) if unit == "player" then TracePrintf(spellId, "%s: %d, lineId=%d", event, spellId, lineId) UnqueueSpellcast(spellId, lineId) end end -- UNIT_SPELLCAST_SENT is triggered when the spellcast is sent to the server. -- This is sent before all other UNIT_SPELLCAST_* events for the same spellcast. function OvaleFuture:UNIT_SPELLCAST_SENT(event, unit, spell, rank, target, lineId) if unit == "player" then self_lastLineID = lineId self_lastSpell = spell -- UNIT_TARGET may arrive out of order with UNIT_SPELLCAST* events, so we can't track -- the target in an event handler. if target then if target == API_UnitName("target") then self_lastTarget = API_UnitGUID("target") else self_lastTarget = OvaleGUID:GetGUIDForName(target) end else self_lastTarget = OVALE_UNKNOWN_GUID end TracePrintf(spell, "%s: %s on %s, lineId=%d", event, spell, self_lastTarget, lineId) end end -- UNIT_SPELLCAST_SUCCEEDED is triggered when a spellcast successfully completes. --[[ For a cast-time spell: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_START UNIT_SPELLCAST_SUCCEEDED For an instant-cast spell: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_SUCCEEDED ]]-- function OvaleFuture:UNIT_SPELLCAST_SUCCEEDED(event, unit, name, rank, lineId, spellId) if unit == "player" and not WHITE_ATTACK[spellId] then self:StartProfiling("OvaleFuture_UNIT_SPELLCAST_SUCCEEDED") TracePrintf(spellId, "%s: %d, lineId=%d", event, spellId, lineId) -- Search for a cast-time spell matching this spellcast that was added by UNIT_SPELLCAST_START. for _, spellcast in ipairs(self_activeSpellcast) do if spellcast.lineId == lineId then spellcast.allowRemove = true -- Take a more recent snapshot of the player stats for this cast-time spell. if spellcast.snapshot then OvalePaperDoll:ReleaseSnapshot(spellcast.snapshot) end spellcast.snapshot = OvalePaperDoll:GetSnapshot() spellcast.damageMultiplier = GetDamageMultiplier(spellId, spellcast.snapshot) local now = API_GetTime() self:SendMessage("Ovale_SpellCast", now, spellcast.spellId, spellcast.target) self:StopProfiling("OvaleFuture_UNIT_SPELLCAST_SUCCEEDED") return end end --[[ This spell was an instant-cast spell, but only add it to the queue if it's not part of a channeled spell. A channeled spell is actually two separate spells, an instant-cast portion and a channel portion, with different line IDs. The instant-cast triggers UNIT_SPELLCAST_SENT and UNIT_SPELLCAST_SUCCEEDED, while the channel triggers UNIT_SPELLCAST_CHANNEL_START and UNIT_SPELLCAST_CHANNEL_STOP. ]]-- if not API_UnitChannelInfo("player") then local now = API_GetTime() local spellcast = QueueSpellcast(spellId, lineId, now, now, false, true) self:SendMessage("Ovale_SpellCast", now, spellId, spellcast.target) end self:StopProfiling("OvaleFuture_UNIT_SPELLCAST_SUCCEEDED") end end function OvaleFuture:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, cleuEvent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...) local arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19, arg20, arg21, arg22, arg23, arg24, arg25 = ... --[[ Sequence of events: - casting a spell that damages SPELL_CAST_START SPELL_DAMAGE - casting a spell that misses SPELL_CAST_START SPELL_MISSED - casting a spell then interrupting it SPELL_CAST_START SPELL_CAST_FAILED - casting an instant damaging spell SPELL_CAST_SUCCESS SPELL_DAMAGE - chanelling a damaging spell SPELL_CAST_SUCCESS SPELL_AURA_APPLIED SPELL_PERIODIC_DAMAGE SPELL_PERIODIC_DAMAGE SPELL_PERIODIC_DAMAGE (interruption does not generate an event) - refreshing a buff SPELL_AURA_REFRESH SPELL_CAST_SUCCESS - removing a buff SPELL_AURA_REMOVED - casting a buff SPELL_AURA_APPLIED SPELL_CAST_SUCCESS -casting a DOT that misses SPELL_CAST_SUCCESS SPELL_MISSED - casting a DOT that damages SPELL_CAST_SUCESS SPELL_AURA_APPLIED SPELL_PERIODIC_DAMAGE SPELL_PERIODIC_DAMAGE ]]-- -- Called when a missile reaches or misses its target if sourceGUID == self_guid then local success = CLEU_SUCCESSFUL_SPELLCAST_EVENT[cleuEvent] --[[ If this is a "SPELL_DAMAGE" event, then only count it as a success if it was the "main" attack, and not an off-hand or multistrike attack. Also change success type to "critical" if true. --]] if cleuEvent == "SPELL_DAMAGE" then local critical, isOffHand, multistrike = arg21, arg24, arg25 if isOffHand or multistrike then success = nil elseif critical then success = "critical" end end if success then self:StartProfiling("OvaleFuture_COMBAT_LOG_EVENT_UNFILTERED") local spellId, spellName = arg12, arg13 TracePrintf(spellId, "%s: %s (%d)", cleuEvent, spellName, spellId) for index, spellcast in ipairs(self_activeSpellcast) do if spellcast.allowRemove and not spellcast.channeled and (spellcast.spellId == spellId or spellcast.auraId == spellId) then spellcast.success = success if spellcast.removeOnSuccess or (spellcast.removeOnAuraSuccess and CLEU_AURA_EVENT[cleuEvent]) or success == "miss" then TracePrintf(spellId, " Spell finished (%s): %s (%d)", success, spellName, spellId) tremove(self_activeSpellcast, index) UpdateLastSpellcast(spellcast) local now = API_GetTime() self.lastCastTime[spellcast.spellId] = now self:SendMessage("Ovale_SpellFinished", now, spellcast.spellId, spellcast.target, success) local unitId = spellcast.target and OvaleGUID:GetUnitId(spellcast.target) or "player" Ovale.refreshNeeded[unitId] = true Ovale.refreshNeeded["player"] = true end break end end self:StopProfiling("OvaleFuture_COMBAT_LOG_EVENT_UNFILTERED") end end end -- Apply the effects of spells that are being cast or are in flight, allowing us to -- ignore lag or missile travel time. function OvaleFuture:ApplyInFlightSpells(state) self:StartProfiling("OvaleFuture_ApplyInFlightSpells") local now = API_GetTime() local index = 1 while index <= #self_activeSpellcast do local spellcast = self_activeSpellcast[index] state:Log("now = %f, spellId = %d, endCast = %f", now, spellcast.spellId, spellcast.stop) if now - spellcast.stop < 5 then state:ApplySpell(spellcast.spellId, spellcast.target, spellcast.start, spellcast.stop, spellcast.channeled, spellcast) else tremove(self_activeSpellcast, index) self_pool:Release(spellcast) -- Decrement current index since item was removed and rest of items shifted up. index = index - 1 end index = index + 1 end self:StopProfiling("OvaleFuture_ApplyInFlightSpells") end function OvaleFuture:LastInFlightSpell() if #self_activeSpellcast > 0 then return self_activeSpellcast[#self_activeSpellcast] end return self.lastGCDSpellcast end function OvaleFuture:UpdateFromSpellcast(dest, spellcast) self:StartProfiling("OvaleFuture_UpdateFromSpellcast") if dest.snapshot then OvalePaperDoll:ReleaseSnapshot(dest.snapshot) end if spellcast.snapshot then dest.snapshot = OvalePaperDoll:GetSnapshot(spellcast.snapshot) end if spellcast.damageMultiplier then dest.damageMultiplier = spellcast.damageMultiplier end -- Update additional information from the spellcast that are registered with this module. for tbl in pairs(self_updateSpellcastInfo) do if tbl.UpdateFromSpellcast then tbl.UpdateFromSpellcast(dest, spellcast) end end self:StopProfiling("OvaleFuture_UpdateFromSpellcast") end function OvaleFuture:GetLastSpellInfo(guid, spellId, statName) if self_lastSpellcast[guid] and self_lastSpellcast[guid][spellId] then if OvalePaperDoll.SNAPSHOT_STATS[statName] then return self_lastSpellcast[guid][spellId].snapshot[statName] else return self_lastSpellcast[guid][spellId][statName] end end end function OvaleFuture:InFlight(spellId) for _, spellcast in ipairs(self_activeSpellcast) do if spellcast.spellId == spellId then return true end end return false end function OvaleFuture:RegisterSpellcastInfo(functionTable) self_updateSpellcastInfo[functionTable] = true end function OvaleFuture:UnregisterSpellcastInfo(functionTable) self_updateSpellcastInfo[functionTable] = nil end function OvaleFuture:DebugSpellsInFlight() if next(self_activeSpellcast) then self:Print("Spells in flight:") else self:Print("No spells in flight!") end for _, spellcast in ipairs(self_activeSpellcast) do self:Print(" %s (%d), lineId=%s", OvaleSpellBook:GetSpellName(spellcast.spellId), spellcast.spellId, spellcast.lineId) end end --</public-static-methods> --[[---------------------------------------------------------------------------- State machine for simulator. --]]---------------------------------------------------------------------------- --<public-static-properties> OvaleFuture.statePrototype = {} --</public-static-properties> --<private-static-properties> local statePrototype = OvaleFuture.statePrototype --</private-static-properties> --<state-properties> -- The in-combat state of the player. statePrototype.inCombat = nil -- The time that combat began. statePrototype.combatStartTime = nil -- The current time in the simulator. statePrototype.currentTime = nil -- The spell being cast in the simulator. statePrototype.currentSpellId = nil -- The starting cast time of the spell being cast in the simulator. statePrototype.startCast = nil -- The ending cast time of the spell being cast in the simulator. statePrototype.endCast = nil -- The time at which the next GCD spell can be cast in the simulator. statePrototype.nextCast = nil -- The most recent time the spell was cast in the simulator. statePrototype.lastCast = nil -- Whether the player is channeling a spell in the simulator at the current time. statePrototype.isChanneling = nil -- The previous spell cast in the simulator. statePrototype.lastSpellId = nil -- The previous GCD spell cast in the simulator. statePrototype.lastGCDSpellId = nil -- counter[name] = count statePrototype.counter = nil --</state-properties> --<public-static-methods> -- Initialize the state. function OvaleFuture:InitializeState(state) state.lastCast = {} state.counter = {} end -- Reset the state to the current conditions. function OvaleFuture:ResetState(state) self:StartProfiling("OvaleFuture_ResetState") local now = API_GetTime() state.currentTime = now state:Log("Reset state with current time = %f", state.currentTime) state.inCombat = self.inCombat state.combatStartTime = self.combatStartTime or 0 state.lastSpellId = self.lastSpellcast and self.lastSpellcast.spellId state.lastGCDSpellId = self.lastGCDSpellcast and self.lastGCDSpellcast.spellId state.currentSpellId = nil state.isChanneling = false state.nextCast = now for k in pairs(state.lastCast) do state.lastCast[k] = nil end for k, v in pairs(self.counter) do state.counter[k] = self.counter[k] end self:StopProfiling("OvaleFuture_ResetState") end -- Release state resources prior to removing from the simulator. function OvaleFuture:CleanState(state) state.inCombat = nil state.currentTime = nil state.currentSpellId = nil state.startCast = nil state.endCast = nil state.nextCast = nil state.isChanneling = nil state.lastSpellId = nil state.lastGCDSpellId = nil for k in pairs(state.lastCast) do state.lastCast[k] = nil end for k in pairs(state.counter) do state.counter[k] = nil end end -- Apply the effects of the spell at the start of the spellcast. function OvaleFuture:ApplySpellStartCast(state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) self:StartProfiling("OvaleFuture_ApplySpellStartCast") local si = OvaleData.spellInfo[spellId] if si then -- Increment and reset spell counters. local target = OvaleGUID:GetUnitId(targetGUID) if si.inccounter then local id = state:GetSpellInfoProperty(spellId, "inccounter", target) if id then local value = state.counter[id] and state.counter[id] or 0 state.counter[id] = value + 1 end end if si.resetcounter then local id = state:GetSpellInfoProperty(spellId, "resetcounter", target) if id then state.counter[id] = 0 end end end self:StopProfiling("OvaleFuture_ApplySpellStartCast") end --</public-static-methods> --<state-methods> statePrototype.GetCounterValue = function(state, id) return state.counter[id] or 0 end statePrototype.GetDamageMultiplier = function(state, spellId) return GetDamageMultiplier(spellId, state.snapshot, state) end statePrototype.TimeOfLastCast = function(state, spellId) return state.lastCast[spellId] or OvaleFuture.lastCastTime[spellId] or 0 end --[[ Cast a spell in the simulator and advance the state of the simulator. Parameters: spellId The ID of the spell to cast. targetGUID The GUID of the target of the spellcast. startCast The time at the start of the spellcast. endCast The time at the end of the spellcast. isChanneled The spell is a channeled spell. spellcast (optional) Table of spellcast information, including a snapshot of player's stats. --]] statePrototype.ApplySpell = function(state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) OvaleFuture:StartProfiling("OvaleFuture_state_ApplySpell") if spellId and targetGUID then local target = OvaleGUID:GetUnitId(targetGUID) local gcd = state:GetGCD(spellId, target) local nextCast -- Handle missing start/end/next cast times. if not startCast or not endCast then local castTime = OvaleSpellBook:GetCastTime(spellId) or 0 startCast = startCast or state.nextCast endCast = endCast or (startCast + castTime) nextCast = (castTime > gcd) and endCast or (startCast + gcd) end -- Update the latest spell cast in the simulator. state.currentSpellId = spellId state.startCast = startCast state.endCast = endCast state.nextCast = nextCast state.isChanneling = isChanneled state.lastCast[spellId] = endCast state.lastSpellId = spellId if gcd > 0 then state.lastGCDSpellId = spellId end -- Set the current time in the simulator to *slightly* after the start of the current cast, -- or to now if in the past. local now = API_GetTime() if startCast >= now then state.currentTime = startCast + SIMULATOR_LAG else state.currentTime = now end state:Log("Apply spell %d at %f currentTime=%f nextCast=%f endCast=%f targetGUID=%s", spellId, startCast, state.currentTime, nextCast, endCast, targetGUID) -- Update the combat state so this condition can be checked in other state prototype methods. -- This condition isn't quite right because casting a harmful spell at a target doesn't always put us into combat. if not state.inCombat and OvaleSpellBook:IsHarmfulSpell(spellId) then state.inCombat = true if isChanneled then state.combatStartTime = startCast else state.combatStartTime = endCast end end --[[ Apply the effects of the spellcast in four phases. 1. Effects at the beginning of the spellcast. 2. Effects when the spell has been cast. 3. Effects when the spellcast hits the target. 4. Effects after the spellcast hits the target (possibly due to server lag). --]] -- If the spellcast has already started, then the effects have already occurred. if startCast > now then OvaleState:InvokeMethod("ApplySpellStartCast", state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) end -- If the spellcast has already ended, then the effects have already occurred. if endCast > now then OvaleState:InvokeMethod("ApplySpellAfterCast", state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) end if not spellcast or not spellcast.success then OvaleState:InvokeMethod("ApplySpellOnHit", state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) end if not spellcast or not spellcast.success or spellcast.success == "hit" or spellcast.success == "critical" then OvaleState:InvokeMethod("ApplySpellAfterHit", state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) end end OvaleFuture:StopProfiling("OvaleFuture_state_ApplySpell") end --</state-methods>