--[[-------------------------------------------------------------------- Ovale Spell Priority Copyright (C) 2012, 2013 Sidoine Copyright (C) 2012, 2013, 2014 Johnny C. Lam This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License in the LICENSE file accompanying this program. --]]-------------------------------------------------------------------- -- The travelling missiles or spells that have been cast but whose effects were not still not applied local _, Ovale = ... local OvaleFuture = Ovale:NewModule("OvaleFuture", "AceEvent-3.0") Ovale.OvaleFuture = OvaleFuture --<private-static-properties> local OvalePool = Ovale.OvalePool -- Forward declarations for module dependencies. local OvaleAura = nil local OvaleData = nil local OvaleGUID = nil local OvalePaperDoll = nil local OvaleScore = nil local OvaleSpellBook = nil local OvaleState = nil local ipairs = ipairs local pairs = pairs local tinsert = table.insert local tostring = tostring local tremove = table.remove 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 -- Profiling set-up. local Profiler = Ovale.Profiler local profiler = nil do local group = OvaleFuture:GetName() local function EnableProfiling() API_GetTime = Profiler:Wrap(group, "OvaleFuture_API_GetTime", GetTime) API_UnitCastingInfo = Profiler:Wrap(group, "OvaleFuture_API_UnitCastingInfo", UnitCastingInfo) API_UnitChannelInfo = Profiler:Wrap(group, "OvaleFuture_API_UnitChannelInfo", UnitChannelInfo) API_UnitGUID = Profiler:Wrap(group, "OvaleFuture_API_UnitGUID", UnitGUID) API_UnitName = Profiler:Wrap(group, "OvaleFuture_API_UnitName", UnitName) end local function DisableProfiling() API_GetTime = GetTime API_UnitCastingInfo = UnitCastingInfo API_UnitChannelInfo = UnitChannelInfo API_UnitGUID = UnitGUID API_UnitName = UnitName end Profiler:RegisterProfilingGroup(group, EnableProfiling, DisableProfiling) profiler = Profiler:GetProfilingGroup(group) end -- 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 --</private-static-properties> --<public-static-properties> --spell counter (see Counter function) OvaleFuture.counter = {} -- Most recent spellcast. OvaleFuture.lastSpellcast = nil -- Debugging: spells to trace OvaleFuture.traceSpellList = nil --</public-static-properties> --<private-static-methods> local function TracePrintf(spellId, ...) local self = OvaleFuture if self.traceSpellList then local name = spellId if type(spellId) == "number" then name = OvaleSpellBook:GetSpellName(spellId) end if self.traceSpellList[spellId] or self.traceSpellList[name] then local now = API_GetTime() Ovale:Printf("[trace] @%f %s", now, Ovale:Format(...)) 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, multiplier in pairs(auraList) do 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 -- 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 AddSpellToQueue(spellId, lineId, startTime, endTime, channeled, allowRemove) profiler.Start("OvaleFuture_AddSpellToQueue") 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 TracePrintf(spellId, " AddSpellToQueue: %s (%d), lineId=%d, startTime=%f, endTime=%f, target=%s", spellName, spellId, lineId, startTime, endTime, spellcast.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 -- Check whether this spell has no cooldown. local buffNoCooldown = si.buff_no_cd or si.buffnocd if buffNoCooldown then local aura = OvaleAura:GetAura("player", buffNoCooldown) if OvaleAura:IsActiveAura(aura) then spellcast.nocd = true end end -- 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 if spellData and (spellData == "refresh" or (type(spellData) == "number" and spellData > 0)) then spellcast.auraId = auraId if spellcast.target ~= self_guid 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 if spellData and (spellData == "refresh" or (type(spellData) == "number" and spellData > 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 self.counter[si.resetcounter] = 0 end if si.inccounter then local prev = self.counter[si.inccounter] or 0 self.counter[si.inccounter] = prev + 1 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 profiler.Stop("OvaleFuture_AddSpellToQueue") end local function RemoveSpellFromQueue(spellId, lineId) profiler.Start("OvaleFuture_RemoveSpellFromQueue") local self = OvaleFuture for index, spellcast in ipairs(self_activeSpellcast) do if spellcast.lineId == lineId then TracePrintf(spellId, " RemoveSpellFromQueue: %s (%d)", OvaleSpellBook:GetSpellName(spellId), spellId) tremove(self_activeSpellcast, index) self_pool:Release(spellcast) break end end Ovale.refreshNeeded["player"] = true profiler.Stop("OvaleFuture_RemoveSpellFromQueue") 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) profiler.Start("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 --[[ 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 profiler.Stop("OvaleFuture_UpdateLastSpellcast") end --</private-static-methods> --<public-static-methods> function OvaleFuture:OnInitialize() -- Resolve module dependencies. OvaleAura = Ovale.OvaleAura 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_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_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 end function OvaleFuture:PLAYER_REGEN_ENABLED(event) self_pool:Drain() 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) AddSpellToQueue(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) RemoveSpellFromQueue(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) AddSpellToQueue(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) RemoveSpellFromQueue(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" then profiler.Start("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) profiler.Stop("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() AddSpellToQueue(spellId, lineId, now, now, false, true) end profiler.Stop("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 = ... --[[ 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 success then profiler.Start("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]) then TracePrintf(spellId, " Spell finished: %s (%d)", spellName, spellId) tremove(self_activeSpellcast, index) UpdateLastSpellcast(spellcast) local unitId = spellcast.target and OvaleGUID:GetUnitId(spellcast.target) or "player" Ovale.refreshNeeded[unitId] = true Ovale.refreshNeeded["player"] = true end break end end profiler.Stop("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) profiler.Start("OvaleFuture_ApplyInFlightSpells") local now = API_GetTime() local index = 1 while index <= #self_activeSpellcast do local spellcast = self_activeSpellcast[index] Ovale:Logf("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.stop, spellcast.channeled, spellcast.nocd, 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 profiler.Stop("OvaleFuture_ApplyInFlightSpells") end function OvaleFuture:LastInFlightSpell() if #self_activeSpellcast > 0 then return self_activeSpellcast[#self_activeSpellcast] end return self.lastSpellcast end function OvaleFuture:UpdateSnapshotFromSpellcast(dest, spellcast) profiler.Start("OvaleFuture_UpdateSnapshotFromSpellcast") 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 profiler.Stop("OvaleFuture_UpdateSnapshotFromSpellcast") 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:Debug() if next(self_activeSpellcast) then Ovale:Print("Spells in flight:") else Ovale:Print("No spells in flight!") end for _, spellcast in ipairs(self_activeSpellcast) do Ovale:FormatPrint(" %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 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 -- 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 -- counter[name] = count statePrototype.counter = nil --</state-properties> --<public-static-methods> -- Initialize the state. function OvaleFuture:InitializeState(state) state.counter = {} end -- Reset the state to the current conditions. function OvaleFuture:ResetState(state) profiler.Start("OvaleFuture_ResetState") local now = API_GetTime() state.currentTime = now Ovale:Logf("Reset state with current time = %f", state.currentTime) state.lastSpellId = self.lastSpellcast and self.lastSpellcast.spellId state.currentSpellId = nil state.isChanneling = false state.nextCast = now for k, v in pairs(self.counter) do state.counter[k] = self.counter[k] end profiler.Stop("OvaleFuture_ResetState") end -- Release state resources prior to removing from the simulator. function OvaleFuture:CleanState(state) state.currentTime = nil state.currentSpellId = nil state.startCast = nil state.endCast = nil state.nextCast = nil state.isChanneling = nil state.lastSpellId = nil 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, nextCast, isChanneled, nocd, spellcast) profiler.Start("OvaleFuture_ApplySpellStartCast") local si = OvaleData.spellInfo[spellId] if si then -- Increment and reset spell counters. if si.inccounter then local id = si.inccounter local value = state.counter[id] and state.counter[id] or 0 state.counter[id] = value + 1 end if si.resetcounter then local id = si.resetcounter state.counter[id] = 0 end end profiler.Stop("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 --[[ 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. nextCast The earliest time at which the next spell can be cast (nextCast >= endCast). isChanneled The spell is a channeled spell. nocd The spell's cooldown is not triggered. spellcast (optional) Table of spellcast information, including a snapshot of player's stats. --]] statePrototype.ApplySpell = function(state, ...) profiler.Start("OvaleFuture_state_ApplySpell") local spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast = ... if spellId and targetGUID then -- Handle missing start/end/next cast times. if not startCast or not endCast or not nextCast then local _, _, _, _, _, _, castTime = API_GetSpellInfo(spellId) castTime = castTime and (castTime / 1000) or 0 local gcd = OvaleCooldown:GetGCD(spellId) 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.lastSpellId = spellId -- Set the current time in the simulator to a little 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 + 0.1 else state.currentTime = now end Ovale:Logf("Apply spell %d at %f currentTime=%f nextCast=%f endCast=%f targetGUID=%s", spellId, startCast, state.currentTime, nextCast, endCast, targetGUID) --[[ 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, ...) end -- If the spellcast has already ended, then the effects have already occurred. if endCast > now then OvaleState:InvokeMethod("ApplySpellAfterCast", state, ...) end if not spellcast or not spellcast.success then OvaleState:InvokeMethod("ApplySpellOnHit", state, ...) end if not spellcast or not spellcast.success or spellcast.success == "hit" then OvaleState:InvokeMethod("ApplySpellAfterHit", state, ...) end end profiler.Stop("OvaleFuture_state_ApplySpell") end --</state-methods>