Quantcast

Simplify casting spells in the simulator.

Johnny C. Lam [11-26-13 - 14:04]
Simplify casting spells in the simulator.

- Push more state into the simulator. This is a step towards allowing
  multiple simulators managed by Ovale.

- Remove OvaleState.now and replace with direct calls to API_GetTime().
  Places that were using OvaleState.now were really needing the current
  time, and the client already caches the current time for API_GetTime()
  for the duration of a frame.

- Re-arrange parameters to ApplySpell*() methods to simplify casting
  spells in the simulator.

git-svn-id: svn://svn.curseforge.net/wow/ovale/mainline/trunk@1205 d5049fe3-3747-40f7-a4b5-f36d6801af5f
Filename
OvaleAura.lua
OvaleBestAction.lua
OvaleComboPoints.lua
OvaleCooldown.lua
OvaleEclipse.lua
OvaleFrame.lua
OvaleFuture.lua
OvaleIcone.lua
OvalePaperDoll.lua
OvalePower.lua
OvaleRunes.lua
OvaleState.lua
conditions/AfterWhiteHit.lua
conditions/Health.lua
conditions/WeaponEnchantExpires.lua
diff --git a/OvaleAura.lua b/OvaleAura.lua
index 97d04e7..8b2d2a0 100644
--- a/OvaleAura.lua
+++ b/OvaleAura.lua
@@ -613,7 +613,7 @@ function OvaleAura:ResetState(state)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvaleAura:ApplySpellAfterCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleAura:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	-- Apply the auras on the player.
 	if si and si.aura and si.aura.player then
@@ -622,11 +622,11 @@ function OvaleAura:ApplySpellAfterCast(state, spellId, startCast, endCast, nextC
 end

 -- Apply the effects of the spell on the target's state when it lands on the target.
-function OvaleAura:ApplySpellOnHit(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleAura:ApplySpellOnHit(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	-- Apply the auras on the target.
 	if si and si.aura and si.aura.target then
-		state:ApplySpellAuras(spellId, startCast, endCast, isChanneled, targetGUID, si.aura.target, spellcast)
+		state:ApplySpellAuras(spellId, targetGUID, startCast, endCast, isChanneled, si.aura.target, spellcast)
 	end
 end
 --</public-static-methods>
@@ -636,7 +636,7 @@ do
 	local statePrototype = OvaleAura.statePrototype

 	-- Apply the auras caused by the given spell in the simulator.
-	statePrototype.ApplySpellAuras = function(state, spellId, startCast, endCast, isChanneled, guid, auraList, spellcast)
+	statePrototype.ApplySpellAuras = function(state, spellId, guid, startCast, endCast, isChanneled, auraList, spellcast)
 		local target = OvaleGUID:GetUnitId(guid)
 		for filter, filterInfo in pairs(auraList) do
 			for auraId, spellData in pairs(filterInfo) do
diff --git a/OvaleBestAction.lua b/OvaleBestAction.lua
index c77bdce..0dfb37d 100644
--- a/OvaleBestAction.lua
+++ b/OvaleBestAction.lua
@@ -45,6 +45,7 @@ local Intersect = OvaleTimeSpan.Intersect
 local IntersectInterval = OvaleTimeSpan.IntersectInterval
 local Measure = OvaleTimeSpan.Measure
 local Union = OvaleTimeSpan.Union
+local API_GetTime = GetTime
 local API_GetActionCooldown = GetActionCooldown
 local API_GetActionTexture = GetActionTexture
 local API_GetItemIcon = GetItemIcon
@@ -124,14 +125,14 @@ local function ComputeAction(element)
 		start = state.currentTime
 	end

-	Ovale:Logf("start=%f nextCast=%s [%d]", start, OvaleState.nextCast, element.nodeId)
+	Ovale:Logf("start=%f nextCast=%s [%d]", start, state.nextCast, element.nodeId)

 	-- If the action is available before the end of the current spellcast, then wait until we can first cast the action.
-	if start < OvaleState.nextCast then
-		local si = OvaleState.currentSpellId and OvaleData.spellInfo[OvaleState.currentSpellId]
+	if start < state.nextCast then
+		local si = state.currentSpellId and OvaleData.spellInfo[state.currentSpellId]
 		if not (si and si.canStopChannelling) then
 			-- not a channelled spell, or a channelled spell that cannot be interrupted
-			start = OvaleState.nextCast
+			start = state.nextCast
 		else
 			-- This is a channelled spell that can be interrupted, so wait till the next tick.
 			-- "canStopChannelling=N" means that there are N total ticks in the channelled spell.
@@ -144,8 +145,8 @@ local function ComputeAction(element)
 				scaling = 1
 			end
 			numTicks = floor(si.canStopChannelling * scaling + 0.5)
-			local tick = (OvaleState.nextCast - OvaleState.startCast) / numTicks
-			local tickTime = OvaleState.startCast + tick
+			local tick = (state.nextCast - state.startCast) / numTicks
+			local tickTime = state.startCast + tick
 			Ovale:Logf("%s start=%f", spellId, start)
 			for i = 1, numTicks do
 				if start <= tickTime then
@@ -717,9 +718,9 @@ function OvaleBestAction:OnInitialize()
 	OvaleState = Ovale.OvaleState
 end

-function OvaleBestAction:StartNewAction()
-	OvaleState:Reset()
-	OvaleFuture:ApplyInFlightSpells()
+function OvaleBestAction:StartNewAction(state)
+	OvaleState:Reset(state)
+	OvaleFuture:ApplyInFlightSpells(state)
 	self_serial = self_serial + 1
 end

@@ -819,7 +820,7 @@ function OvaleBestAction:GetActionInfo(element)
 		local texture = element.params[1]
 		actionTexture = "Interface\\Icons\\" .. texture
 		actionInRange = nil
-		actionCooldownStart = OvaleState.now
+		actionCooldownStart = API_GetTime()
 		actionCooldownDuration = 0
 		actionEnable = 1
 		actionUsable = true
diff --git a/OvaleComboPoints.lua b/OvaleComboPoints.lua
index 5b3fb75..4faae32 100644
--- a/OvaleComboPoints.lua
+++ b/OvaleComboPoints.lua
@@ -129,7 +129,7 @@ function OvaleComboPoints:ResetState(state)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvaleComboPoints:ApplySpellAfterCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleComboPoints:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	if si and si.combo then
 		local cost = si.combo
diff --git a/OvaleCooldown.lua b/OvaleCooldown.lua
index edcbd4b..6d0ed57 100644
--- a/OvaleCooldown.lua
+++ b/OvaleCooldown.lua
@@ -113,7 +113,7 @@ function OvaleCooldown:ResetState(state)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvaleCooldown:ApplySpellAfterCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleCooldown:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	if si then
 		local cd = state:GetCD(spellId)
diff --git a/OvaleEclipse.lua b/OvaleEclipse.lua
index 71d9467..3e7ff6f 100644
--- a/OvaleEclipse.lua
+++ b/OvaleEclipse.lua
@@ -144,7 +144,7 @@ function OvaleEclipse:ResetState(state)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvaleEclipse:ApplySpellAfterCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleEclipse:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	if si and si.eclipse then
 		local eclipse = state.eclipse
diff --git a/OvaleFrame.lua b/OvaleFrame.lua
index a6974a0..46a528a 100644
--- a/OvaleFrame.lua
+++ b/OvaleFrame.lua
@@ -138,7 +138,8 @@ do
 					-- print("sort "..spellId.." parfait")
 					return 1
 				else
-					local lag = OvaleState.now - action.waitStart
+					local now = API_GetTime()
+					local lag = now - action.waitStart
 					if lag>5 then
 					-- 	print("sort "..spellId.." ignoré (>5s)")
 						return nil
@@ -173,8 +174,8 @@ do

 		self.lastUpdate = now

-		OvaleState:StartNewFrame()
 		local state = OvaleState.state
+		OvaleState:StartNewFrame(state)
 		for k,node in pairs(OvaleCompile.masterNodes) do
 			local target
 			if node.params and node.params.target then
@@ -186,7 +187,7 @@ do

 			if forceRefresh or Ovale.refreshNeeded[target] or Ovale.refreshNeeded["player"] or Ovale.refreshNeeded["pet"] then
 				Ovale:Logf("****Master Node %d", k)
-				OvaleBestAction:StartNewAction()
+				OvaleBestAction:StartNewAction(state)
 				local timeSpan, _, element = OvaleBestAction:Compute(node)
 				local start = NextTime(timeSpan, state.currentTime)
 				if start then
@@ -206,7 +207,7 @@ do
 					end
 					local value
 					if element.value and element.origin and element.rate then
-						value = element.value + (OvaleState.now - element.origin) * element.rate
+						value = element.value + (now - element.origin) * element.rate
 					end
 					icons[1]:SetValue(value, actionTexture)
 					if #icons > 1 then
@@ -222,10 +223,10 @@ do
 						end
 					end
 						-- Dans le cas de canStopChannelling, on risque de demander d'interrompre le channelling courant, ce qui est stupide
-					if start and OvaleState.currentSpellId and OvaleState.nextCast and spellId == OvaleState.currentSpellId and start < OvaleState.nextCast then
-						start = OvaleState.nextCast
+					if start and state.currentSpellId and state.nextCast and spellId == state.currentSpellId and start < state.nextCast then
+						start = state.nextCast
 					end
-						if (node.params.nocd and start~=nil and OvaleState.now<start-node.params.nocd) then
+						if (node.params.nocd and start~=nil and now < start - node.params.nocd) then
 						icons[1]:Update(element, nil)
 					else
 						icons[1]:Update(element, start, actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
@@ -233,16 +234,16 @@ do
 					end

 					action.spellId = spellId
-					if start == OvaleState.now and actionUsable then
+					if start and start <= now and actionUsable then
 						if not action.waitStart then
-							action.waitStart = OvaleState.now
+							action.waitStart = now
 						end
 					else
 						action.waitStart = nil
 					end

 					if profile.apparence.moving and icons[1].debutAction and icons[1].finAction then
-						local top=1-(OvaleState.now - icons[1].debutAction)/(icons[1].finAction-icons[1].debutAction)
+						local top=1-(now - icons[1].debutAction)/(icons[1].finAction-icons[1].debutAction)
 						if top<0 then
 							top = 0
 						elseif top>1 then
@@ -256,29 +257,15 @@ do

 					if (node.params.size ~= "small" and not node.params.nocd and profile.apparence.predictif) then
 						if start then
-							local castTime=0
-							if spellId then
-								local _, _, _, _, _, _, _castTime = API_GetSpellInfo(spellId)
-								if _castTime and _castTime>0 then
-									castTime = _castTime/1000
-								end
-							end
-							local gcd = OvaleCooldown:GetGCD(spellId)
-							local nextCast
-							if (castTime>gcd) then
-								nextCast = start + castTime
-							else
-								nextCast = start + gcd
-							end
 							Ovale:Logf("****Second icon %f", start)
 							local spellTarget
 							if element then
 								spellTarget = element.params.target
 							end
-							if spellTarget == "target" or not spellTarget then
-								spellTarget = target
+							if not spellTarget or spellTarget == "target" then
+								spellTarget = OvaleCondition.defaultTarget
 							end
-							OvaleState:ApplySpell(spellId, start, start + castTime, nextCast, false, false, OvaleGUID:GetGUID(spellTarget))
+							OvaleState:ApplySpell(state, spellId, OvaleGUID:GetGUID(spellTarget))
 							timeSpan, _, element = OvaleBestAction:Compute(node)
 							start = NextTime(timeSpan, state.currentTime)
 							icons[2]:Update(element, start, OvaleBestAction:GetActionInfo(element))
diff --git a/OvaleFuture.lua b/OvaleFuture.lua
index 45e9e7a..a529320 100644
--- a/OvaleFuture.lua
+++ b/OvaleFuture.lua
@@ -492,8 +492,8 @@ 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()
-	local now = OvaleState.now
+function OvaleFuture:ApplyInFlightSpells(state)
+	local now = API_GetTime()
 	local index = 0
 	while true do
 		index = index + 1
@@ -502,7 +502,7 @@ function OvaleFuture:ApplyInFlightSpells()
 		local spellcast = self_activeSpellcast[index]
 		Ovale:Logf("now = %f, spellId = %d, endCast = %f", now, spellcast.spellId, spellcast.stop)
 		if now - spellcast.stop < 5 then
-			OvaleState:ApplySpell(spellcast.spellId, spellcast.start, spellcast.stop, spellcast.stop, spellcast.channeled, spellcast.nocd, spellcast.target, spellcast)
+			OvaleState:ApplySpell(state, spellcast.spellId, spellcast.target, spellcast.start, spellcast.stop, spellcast.stop, spellcast.channeled, spellcast.nocd, spellcast)
 		else
 			tremove(self_activeSpellcast, index)
 			self_pool:Release(spellcast)
@@ -585,7 +585,7 @@ function OvaleFuture:ResetState(state)
 end

 -- Apply the effects of the spell at the start of the spellcast.
-function OvaleFuture:ApplySpellStartCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleFuture:ApplySpellStartCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	if si then
 		-- Increment and reset spell counters.
diff --git a/OvaleIcone.lua b/OvaleIcone.lua
index 8ead071..1c66742 100644
--- a/OvaleIcone.lua
+++ b/OvaleIcone.lua
@@ -23,6 +23,7 @@ local pairs = pairs
 local strfind = string.find
 local strsub = string.sub
 local tostring = tostring
+local API_GetTime = GetTime
 --</private-static-properties>

 --<public-methods>
@@ -53,23 +54,24 @@ end

 local function Update(self, element, minAttente, actionTexture, actionInRange, actionCooldownStart, actionCooldownDuration,
 				actionUsable, actionShortcut, actionIsCurrent, actionEnable, spellId, actionTarget)
-
 	self.spellId = spellId
 	self.value = nil

+	local now = API_GetTime()
+	local state = OvaleState.state
 	local profile = OvaleOptions:GetProfile()
 	if (minAttente~=nil and actionTexture) then

 		if (actionTexture~=self.actionCourante or self.ancienneAttente==nil or
-			(minAttente~=OvaleState.now and minAttente>self.ancienneAttente+0.01) or
+			(minAttente~=now and minAttente>self.ancienneAttente+0.01) or
 			(minAttente < self.finAction-0.01)) then
 			if (actionTexture~=self.actionCourante or self.ancienneAttente==nil or
-					(minAttente~=OvaleState.now and minAttente>self.ancienneAttente+0.01)) then
-				self.debutAction = OvaleState.now
+					(minAttente~=now and minAttente>self.ancienneAttente+0.01)) then
+				self.debutAction = now
 			end
 			self.actionCourante = actionTexture
 			self.finAction = minAttente
-			if (minAttente == OvaleState.now) then
+			if (minAttente == now) then
 				self.cd:Hide()
 			else
 				self.lastSound = nil
@@ -80,7 +82,7 @@ local function Update(self, element, minAttente, actionTexture, actionInRange, a
 			end
 		end

-		if not profile.apparence.flashIcon and minAttente<=OvaleState.now then
+		if not profile.apparence.flashIcon and minAttente<=now then
 			self.cd:Hide()
 		end

@@ -98,21 +100,21 @@ local function Update(self, element, minAttente, actionTexture, actionInRange, a

 		local red
 		if minAttente > actionCooldownStart + actionCooldownDuration + 0.01
-				and minAttente > OvaleState.now
-				and minAttente > OvaleState.nextCast then
+				and minAttente > now
+				and minAttente > state.nextCast then
 			self.icone:SetVertexColor(0.75,0.2,0.2)
 			red = true
 		else
 			self.icone:SetVertexColor(1,1,1)
 		end

-		--if (minAttente==OvaleState.now) then
+		--if (minAttente==now) then
 			--self.cd:Hide()
 		--end

 		if element.params.sound and not self.lastSound then
 			local delay = element.params.soundtime or 0.5
-			if OvaleState.now>=minAttente - delay then
+			if now>=minAttente - delay then
 				self.lastSound = element.params.sound
 			--	print("Play" .. self.lastSound)
 				PlaySoundFile(self.lastSound)
@@ -120,10 +122,10 @@ local function Update(self, element, minAttente, actionTexture, actionInRange, a
 		end

 		-- La latence
-		if minAttente>OvaleState.now and profile.apparence.highlightIcon and not red then
+		if minAttente>now and profile.apparence.highlightIcon and not red then
 			local lag = 0.6
 			local newShouldClick
-			if minAttente<OvaleState.now + lag then
+			if minAttente<now + lag then
 				newShouldClick = true
 			else
 				newShouldClick = false
@@ -142,8 +144,8 @@ local function Update(self, element, minAttente, actionTexture, actionInRange, a
 		end

 		-- Le temps restant
-		if ((profile.apparence.numeric or self.params.text == "always") and minAttente > OvaleState.now) then
-			self.remains:SetFormattedText("%.1f", minAttente - OvaleState.now)
+		if ((profile.apparence.numeric or self.params.text == "always") and minAttente > now) then
+			self.remains:SetFormattedText("%.1f", minAttente - now)
 			self.remains:Show()
 		else
 			self.remains:Hide()
diff --git a/OvalePaperDoll.lua b/OvalePaperDoll.lua
index d308d64..aa2cbd7 100644
--- a/OvalePaperDoll.lua
+++ b/OvalePaperDoll.lua
@@ -500,7 +500,8 @@ end
 function OvalePaperDoll:ResetState(state)
 	state.level = self.level
 	state.specialization = self.specialization
-	if state.snapshot and state.snapshot.snapshotTime < OvaleState.now then
+	local now = API_GetTime()
+	if state.snapshot and state.snapshot.snapshotTime < now then
 		state.snapshot:ReleaseReference()
 		state.snapshot = nil
 	end
diff --git a/OvalePower.lua b/OvalePower.lua
index 9a68488..dded77d 100644
--- a/OvalePower.lua
+++ b/OvalePower.lua
@@ -243,7 +243,7 @@ function OvalePower:ResetState(state)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvalePower:ApplySpellAfterCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvalePower:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]

 	-- Update power using information from GetSpellInfo() if there is no SpellInfo() for the spell's cost.
diff --git a/OvaleRunes.lua b/OvaleRunes.lua
index 720e1f7..a21f9c2 100644
--- a/OvaleRunes.lua
+++ b/OvaleRunes.lua
@@ -204,13 +204,13 @@ function OvaleRunes:ResetState(state)
 end

 -- Apply the effects of the spell on the player's state, assuming the spellcast completes.
-function OvaleRunes:ApplySpellAfterCast(state, spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast)
+function OvaleRunes:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast)
 	local si = OvaleData.spellInfo[spellId]
 	if si then
 		for i, name in ipairs(RUNE_NAME) do
 			local count = si[name] or 0
 			while count > 0 do
-				local attime = isChanneled and startCast or endCast
+				local atTime = isChanneled and startCast or endCast
 				state:ConsumeRune(atTime, name)
 				count = count - 1
 			end
diff --git a/OvaleState.lua b/OvaleState.lua
index a62fb8c..cc4dcfa 100644
--- a/OvaleState.lua
+++ b/OvaleState.lua
@@ -28,30 +28,13 @@ local API_GetTime = GetTime

 local self_statePrototype = {}
 local self_stateModules = OvaleQueue:NewQueue("OvaleState_stateModules")
-
--- Whether the state of the simulator has been initialized.
-local self_stateIsInitialized = false
 --</private-static-properties>

 --<public-static-properties>
--- The spell being cast.
-OvaleState.currentSpellId = nil
-OvaleState.now = nil
-OvaleState.nextCast = nil
-OvaleState.startCast = nil
-OvaleState.endCast = nil
-OvaleState.isChanneling = nil
-OvaleState.lastSpellId = nil
-
 -- The state for the simulator.
-OvaleState.state = {
-	currentTime = nil,
-}
+OvaleState.state = {}
 --</public-static-properties>

---<private-static-methods>
---</private-static-methods>
-
 --<public-static-methods>
 function OvaleState:OnInitialize()
 	-- Resolve module dependencies.
@@ -59,6 +42,15 @@ function OvaleState:OnInitialize()
 	OvaleFuture = Ovale.OvaleFuture
 end

+function OvaleState:OnEnable()
+	self:RegisterState(self, self.statePrototype)
+end
+
+function OvaleState:OnDisable()
+	self:UnregisterState(self)
+end
+
+
 function OvaleState:RegisterState(addon, statePrototype)
 	self_stateModules:Insert(addon)
 	self_statePrototype[addon] = statePrototype
@@ -95,29 +87,14 @@ function OvaleState:InvokeMethod(methodName, ...)
 	end
 end

-function OvaleState:StartNewFrame()
-	if not self_stateIsInitialized then
-		self:InitializeState()
+function OvaleState:StartNewFrame(state)
+	if not state.isInitialized then
+		self:InvokeMethod("InitializeState", state)
 	end
-	self.now = API_GetTime()
-end
-
-function OvaleState:InitializeState()
-	self:InvokeMethod("InitializeState", self.state)
-	self_stateIsInitialized = true
 end

-function OvaleState:Reset()
-	local state = self.state
-	state.currentTime = self.now
-	Ovale:Logf("Reset state with current time = %f", state.currentTime)
-
-	self.lastSpellId = OvaleFuture.lastSpellcast and OvaleFuture.lastSpellcast.spellId
-	self.currentSpellId = nil
-	self.isChanneling = false
-	self.nextCast = self.now
-
-	self:InvokeMethod("ResetState", self.state)
+function OvaleState:Reset(state)
+	self:InvokeMethod("ResetState", state)
 end

 --[[
@@ -125,38 +102,50 @@ end

 	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.
-		targetGUID	The GUID of the target of the spellcast.
 		spellcast	(optional) Table of spellcast information, including a snapshot of player's stats.
 --]]
-function OvaleState:ApplySpell(...)
-	local spellId, startCast, endCast, nextCast, isChanneled, nocd, targetGUID, spellcast = ...
+function OvaleState:ApplySpell(state, ...)
+	local spellId, targetGUID, startCast, endCast, nextCast, isChanneled, nocd, spellcast = ...
 	if not spellId or not targetGUID then
 		return
 	end

+	-- Handle missing start/end/next cast times.
+	if not startCast or not endCast or not nextCast then
+		local castTime = 0
+		local castTime = select(7, 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.
-	local state = self.state
-	self.nextCast = nextCast
-	self.currentSpellId = spellId
-	self.startCast = startCast
-	self.endCast = endCast
-	self.isChanneling = isChanneled
-	self.lastSpellId = spellId
+	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.
-	if startCast >= self.now then
+	local now = API_GetTime()
+	if startCast >= now then
 		state.currentTime = startCast + 0.1
 	else
-		state.currentTime = self.now
+		state.currentTime = now
 	end

-	Ovale:Logf("Apply spell %d at %f currentTime=%f nextCast=%f endCast=%f targetGUID=%s", spellId, startCast, state.currentTime, self.nextCast, endCast, targetGUID)
+	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 three phases.
@@ -165,13 +154,57 @@ function OvaleState:ApplySpell(...)
 			3. Effects when the spellcast hits the target.
 	--]]
 	-- If the spellcast has already started, then the effects have already occurred.
-	if startCast >= OvaleState.now then
-		self:InvokeMethod("ApplySpellStartCast", self.state, ...)
+	if startCast >= now then
+		self:InvokeMethod("ApplySpellStartCast", state, ...)
 	end
 	-- If the spellcast has already ended, then the effects have already occurred.
-	if endCast > OvaleState.now then
-		self:InvokeMethod("ApplySpellAfterCast", self.state, ...)
+	if endCast > now then
+		self:InvokeMethod("ApplySpellAfterCast", state, ...)
 	end
-	self:InvokeMethod("ApplySpellOnHit", self.state, ...)
+	self:InvokeMethod("ApplySpellOnHit", state, ...)
+end
+--</public-static-methods>
+
+--[[----------------------------------------------------------------------------
+	State machine for simulator.
+--]]----------------------------------------------------------------------------
+
+--<public-static-properties>
+OvaleState.statePrototype = {
+	-- Whether the state of the simulator has been initialized.
+	isInitialized = nil,
+	-- The current time in the simulator.
+	currentTime = nil,
+	-- The spell being cast in the simulator.
+	currentSpellId = nil,
+	-- The starting cast time of the spell being cast in the simulator.
+	startCast = nil,
+	-- The ending cast time of the spell being cast in the simulator.
+	endCast = nil,
+	-- The time at which the next GCD spell can be cast in the simulator.
+	nextCast = nil,
+	-- Whether the player is channeling a spell in the simulator at the current time.
+	isChanneling = nil,
+	-- The previous spell cast in the simulator.
+	lastSpellId = nil,
+}
+--</public-static-properties>
+
+--<public-static-methods>
+-- Initialize the state.
+function OvaleState:InitializeState(state)
+	state.isInitialized = true
+end
+
+-- Reset the state to the current conditions.
+function OvaleState:ResetState(state)
+	local now = API_GetTime()
+	state.currentTime = now
+	Ovale:Logf("Reset state with current time = %f", state.currentTime)
+
+	state.lastSpellId = OvaleFuture.lastSpellcast and OvaleFuture.lastSpellcast.spellId
+	state.currentSpellId = nil
+	state.isChanneling = false
+	state.nextCast = now
 end
 --</public-static-methods>
diff --git a/conditions/AfterWhiteHit.lua b/conditions/AfterWhiteHit.lua
index 597b3ef..48352b8 100644
--- a/conditions/AfterWhiteHit.lua
+++ b/conditions/AfterWhiteHit.lua
@@ -12,9 +12,9 @@ local _, Ovale = ...

 do
 	local OvaleCondition = Ovale.OvaleCondition
-	local OvaleState = Ovale.OvaleState
 	local OvaleSwing = Ovale.OvaleSwing

+	local API_GetTime = GetTime
 	local TestValue = OvaleCondition.TestValue

 	-- Test if a white hit just occured
@@ -25,7 +25,7 @@ do
 		local seconds, comparator, limit = condition[1], condition[2], condition[3]
 		local start = OvaleSwing.starttime
 		local ending = start + OvaleSwing.duration
-		local now = OvaleState.now
+		local now = API_GetTime()
 		local value
 		if now - start < seconds then
 			value = 0
diff --git a/conditions/Health.lua b/conditions/Health.lua
index e28272b..f84fac4 100644
--- a/conditions/Health.lua
+++ b/conditions/Health.lua
@@ -13,9 +13,9 @@ local _, Ovale = ...
 do
 	local OvaleCondition = Ovale.OvaleCondition
 	local OvaleGUID = Ovale.OvaleGUID
-	local OvaleState = Ovale.OvaleState

 	local floor = math.floor
+	local API_GetTime = GetTime
 	local API_UnitHealth = UnitHealth
 	local API_UnitHealthMax = UnitHealthMax
 	local Compare = OvaleCondition.Compare
@@ -31,6 +31,7 @@ do
 	--[[
 		Returns:
 			Estimated number of seconds before the specified unit reaches zero health
+			The current time
 			Unit's current health
 			Unit's maximum health
 	--]]
@@ -50,6 +51,7 @@ do
 		local timeToDie
 		local health = API_UnitHealth(unitId) or 0
 		local maxHealth = API_UnitHealthMax(unitId) or 1
+		local currentTime = API_GetTime()

 		-- Clamp maxHealth to always be at least 1.
 		if maxHealth < health then
@@ -65,7 +67,7 @@ do
 			Ovale:Log("Training Dummy, return in the future")
 			timeToDie = math.huge
 		else
-			local now = floor(OvaleState.now)
+			local now = floor(currentTime)
 			if (not lastTTDTime[unitId] or lastTTDTime[unitId] < now) and lastTTDguid[unitId] then
 				lastTTDTime[unitId] = now
 				local mod10, prevHealth
@@ -94,7 +96,7 @@ do
 			-- Return time to die in the far-off future (one week).
 			timeToDie = 3600 * 24 * 7
 		end
-		return timeToDie, health, maxHealth
+		return timeToDie, currentTime, health, maxHealth
 	end

 	--- Get the current amount of health points of the target.
@@ -115,14 +117,14 @@ do
 	local function Health(condition)
 		local comparator, limit = condition[1], condition[2]
 		local target = ParseCondition(condition)
-		local timeToDie, health, maxHealth = EstimatedTimeToDie(target)
+		local timeToDie, now, health, maxHealth = EstimatedTimeToDie(target)
 		if not timeToDie then
 			return nil
 		elseif timeToDie == 0 then
 			return Compare(0, comparator, limit)
 		end
-		local value, origin, rate = health, OvaleState.now, -1 * health / timeToDie
-		local start, ending = OvaleState.now, math.huge
+		local value, origin, rate = health, now, -1 * health / timeToDie
+		local start, ending = now, math.huge
 		return TestValue(start, ending, value, origin, rate, comparator, limit)
 	end

@@ -147,13 +149,13 @@ do
 	local function HealthMissing(condition)
 		local comparator, limit = condition[1], condition[2]
 		local target = ParseCondition(condition)
-		local timeToDie, health, maxHealth = EstimatedTimeToDie(target)
+		local timeToDie, now, health, maxHealth = EstimatedTimeToDie(target)
 		if not timeToDie or timeToDie == 0 then
 			return nil
 		end
 		local missing = maxHealth - health
-		local value, origin, rate = missing, OvaleState.now, health / timeToDie
-		local start, ending = OvaleState.now, math.huge
+		local value, origin, rate = missing, now, health / timeToDie
+		local start, ending = now, math.huge
 		return TestValue(start, ending, value, origin, rate, comparator, limit)
 	end

@@ -178,15 +180,15 @@ do
 	local function HealthPercent(condition)
 		local comparator, limit = condition[1], condition[2]
 		local target = ParseCondition(condition)
-		local timeToDie, health, maxHealth = EstimatedTimeToDie(target)
+		local timeToDie, now, health, maxHealth = EstimatedTimeToDie(target)
 		if not timeToDie then
 			return nil
 		elseif timeToDie == 0 then
 			return Compare(0, comparator, limit)
 		end
 		local healthPercent = health / maxHealth * 100
-		local value, origin, rate = healthPercent, OvaleState.now, -1 * healthPercent / timeToDie
-		local start, ending = OvaleState.now, math.huge
+		local value, origin, rate = healthPercent, now, -1 * healthPercent / timeToDie
+		local start, ending = now, math.huge
 		return TestValue(start, ending, value, origin, rate, comparator, limit)
 	end

@@ -233,9 +235,9 @@ do
 	local function TimeToDie(condition)
 		local comparator, limit = condition[1], condition[2]
 		local target = ParseCondition(condition)
-		local timeToDie = EstimatedTimeToDie(target)
-		local value, origin, rate = timeToDie, OvaleState.now, -1
-		local start, ending = OvaleState.now, OvaleState.now + timeToDie
+		local timeToDie, now = EstimatedTimeToDie(target)
+		local value, origin, rate = timeToDie, now, -1
+		local start, ending = now, now + timeToDie
 		return TestValue(start, ending, value, origin, rate, comparator, limit)
 	end

@@ -260,12 +262,12 @@ do
 	local function TimeToHealthPercent(condition)
 		local percent, comparator, limit = condition[1], condition[2], condition[3]
 		local target = ParseCondition(condition)
-		local timeToDie, health, maxHealth = EstimatedTimeToDie(target)
+		local timeToDie, now, health, maxHealth = EstimatedTimeToDie(target)
 		local healthPercent = health / maxHealth * 100
 		if healthPercent >= percent then
 			local t = timeToDie * (healthPercent - percent) / healthPercent
-			local value, origin, rate = t, OvaleState.now, -1
-			local start, ending = OvaleState.now, OvaleState.now + t
+			local value, origin, rate = t, now, -1
+			local start, ending = now, now + t
 			return TestValue(start, ending, value, origin, rate, comparator, limit)
 		end
 		return Compare(0, comparator, limit)
diff --git a/conditions/WeaponEnchantExpires.lua b/conditions/WeaponEnchantExpires.lua
index d4c9fdc..861acc2 100644
--- a/conditions/WeaponEnchantExpires.lua
+++ b/conditions/WeaponEnchantExpires.lua
@@ -12,8 +12,8 @@ local _, Ovale = ...

 do
 	local OvaleCondition = Ovale.OvaleCondition
-	local OvaleState = Ovale.OvaleState

+	local API_GetTime = GetTime
 	local API_GetWeaponEnchantInfo = GetWeaponEnchantInfo

 	--- Test if the weapon imbue on the given weapon has expired or will expire after a given number of seconds.
@@ -31,18 +31,19 @@ do
 		local hand, seconds = condition[1], condition[2]
 		seconds = seconds or 0
 		local hasMainHandEnchant, mainHandExpiration, _, hasOffHandEnchant, offHandExpiration = API_GetWeaponEnchantInfo()
+		local now = API_GetTime()
 		if hand == "mainhand" or hand == "main" then
 			if not hasMainHandEnchant then
 				return 0, math.huge
 			end
 			mainHandExpiration = mainHandExpiration / 1000
-			return OvaleState.now + mainHandExpiration - seconds, math.huge
+			return now + mainHandExpiration - seconds, math.huge
 		else
 			if not hasOffHandEnchant then
 				return 0, math.huge
 			end
 			offHandExpiration = offHandExpiration / 1000
-			return OvaleState.now + offHandExpiration - seconds, math.huge
+			return now + offHandExpiration - seconds, math.huge
 		end
 	end