--[[-------------------------------------------------------------------- Copyright (C) 2012, 2013, 2014 Johnny C. Lam. See the file LICENSE.txt for copying permission. --]]-------------------------------------------------------------------- -- This addon tracks the number of combo points on the player. local OVALE, Ovale = ... local OvaleComboPoints = Ovale:NewModule("OvaleComboPoints", "AceEvent-3.0") Ovale.OvaleComboPoints = OvaleComboPoints --<private-static-properties> local L = Ovale.L local OvaleDebug = Ovale.OvaleDebug local OvaleProfiler = Ovale.OvaleProfiler -- Forward declarations for module dependencies. local OvaleAura = nil local OvaleData = nil local OvaleFuture = nil local OvaleGUID = nil local OvaleSpellBook = nil local OvaleState = nil local tinsert = table.insert local tremove = table.remove local API_GetTime = GetTime local API_UnitCanAttack = UnitCanAttack local API_UnitClass = UnitClass local API_UnitGUID = UnitGUID local API_UnitPower = UnitPower local MAX_COMBO_POINTS = MAX_COMBO_POINTS local UNKNOWN = UNKNOWN -- Register for debugging messages. OvaleDebug:RegisterDebugging(OvaleComboPoints) -- Register for profiling. OvaleProfiler:RegisterProfiling(OvaleComboPoints) -- Player's class. local _, self_class = API_UnitClass("player") -- Player's GUID. local self_guid = nil -- Rogue's Anticipation talent. local ANTICIPATION = 115189 local ANTICIPATION_DURATION = 15 local ANTICIPATION_TALENT = 18 local self_hasAnticipation = false -- Rogue's Ruthlessness passive spell. local RUTHLESSNESS = 14161 local self_hasRuthlessness = false -- Queue of pending combo point events. local self_pendingComboEvents = {} -- Number of seconds a pending combo point event can exist without expiring. local PENDING_THRESHOLD = 0.8 -- Table of functions to update spellcast information to register with OvaleFuture. local self_updateSpellcastInfo = {} --</private-static-properties> --<public-static-properties> -- The current number of combo points on the player. OvaleComboPoints.combo = 0 --</public-static-properties> --<private-static-methods> -- Add a pending combo event caused by the given spell. local function AddPendingComboEvent(atTime, spellId, guid, reason, combo) local comboEvent = { atTime = atTime, spellId = spellId, guid = guid, reason = reason, combo = combo, } tinsert(self_pendingComboEvents, comboEvent) end -- Remove all pending combo point events caused by the given spell on the target GUID. -- If only atTime is given, then all expired events are removed. local function RemovePendingComboEvents(atTime, spellId, guid, reason, combo) local count = 0 for k = #self_pendingComboEvents, 1, -1 do local comboEvent = self_pendingComboEvents[k] -- Remove expired or matching pending events. if (atTime and atTime - comboEvent.atTime > PENDING_THRESHOLD) or (comboEvent.spellId == spellId and comboEvent.guid == guid and (not reason or comboEvent.reason == reason) and (not combo or comboEvent.combo == combo)) then if comboEvent.combo == "finisher" then OvaleComboPoints:Debug("Removing expired %s event: spell %d combo point finisher from %s.", comboEvent.reason, comboEvent.spellId, comboEvent.reason) else OvaleComboPoints:Debug("Removing expired %s event: spell %d for %d combo points from %s.", comboEvent.reason, comboEvent.spellId, comboEvent.combo, comboEvent.reason) end count = count + 1 tremove(self_pendingComboEvents, k) end end return count end -- Manage spellcast.combo information. local function SaveToSpellcast(spellcast) local spellId = spellcast.spellId if spellId then local si = OvaleData.spellInfo[spellId] if si.combo == "finisher" then local target = OvaleGUID:GetUnitId(spellcast.target) local combo = OvaleData:GetSpellInfoProperty(spellId, "combo", target) if combo == "finisher" then local min_combo = si.min_combo or si.mincombo or 1 if OvaleComboPoints.combo >= min_combo then combo = OvaleComboPoints.combo else combo = min_combo end elseif combo == 0 then -- If this is a finisher that costs no combo points, then treat it as a maximum combo-point finisher. combo = MAX_COMBO_POINTS end spellcast.combo = combo end end end local function UpdateFromSpellcast(dest, spellcast) if spellcast.combo then dest.combo = spellcast.combo end end do self_updateSpellcastInfo.SaveToSpellcast = SaveToSpellcast self_updateSpellcastInfo.UpdateFromSpellcast = UpdateFromSpellcast end --</private-static-methods> --<public-static-methods> function OvaleComboPoints:OnInitialize() -- Resolve module dependencies. OvaleAura = Ovale.OvaleAura OvaleData = Ovale.OvaleData OvaleFuture = Ovale.OvaleFuture OvaleGUID = Ovale.OvaleGUID OvaleSpellBook = Ovale.OvaleSpellBook OvaleState = Ovale.OvaleState end function OvaleComboPoints:OnEnable() self_guid = API_UnitGUID("player") if self_class == "ROGUE" or self_class == "DRUID" then self:RegisterEvent("PLAYER_ENTERING_WORLD", "Update") self:RegisterEvent("PLAYER_TARGET_CHANGED") self:RegisterEvent("UNIT_COMBO_POINTS") self:RegisterMessage("Ovale_SpellFinished") self:RegisterMessage("Ovale_TalentsChanged") OvaleData:RegisterRequirement("combo", "RequireComboPointsHandler", self) OvaleState:RegisterState(self, self.statePrototype) OvaleFuture:RegisterSpellcastInfo(self_updateSpellcastInfo) end end function OvaleComboPoints:OnDisable() if self_class == "ROGUE" or self_class == "DRUID" then OvaleState:UnregisterState(self) OvaleFuture:UnregisterSpellcastInfo(self_updateSpellcastInfo) OvaleData:UnregisterRequirement("combo") self:UnregisterEvent("PLAYER_ENTERING_WORLD") self:UnregisterEvent("PLAYER_TARGET_CHANGED") self:UnregisterEvent("UNIT_COMBO_POINTS") self:UnregisterMessage("Ovale_SpellFinished") self:UnregisterMessage("Ovale_TalentsChanged") end end function OvaleComboPoints:PLAYER_TARGET_CHANGED(event, cause) if cause == "NIL" or cause == "down" then -- Target was cleared. else -- Target has changed. self:Update() end end function OvaleComboPoints:UNIT_COMBO_POINTS(event, unitId) if unitId == "player" then -- Save the old combo point count and update to the current count. local oldCombo = self.combo self:Update() local difference = self.combo - oldCombo self:Debug(true, "%s: %d -> %d.", event, oldCombo, self.combo) -- Remove expired events. local now = API_GetTime() RemovePendingComboEvents(now) local pendingMatched = false if #self_pendingComboEvents > 0 then local comboEvent = self_pendingComboEvents[1] local spellId, guid, reason, combo = comboEvent.spellId, comboEvent.guid, comboEvent.reason, comboEvent.combo if combo == difference or (combo == "finisher" and self.combo == 0 and difference < 0) then self:Debug(" Matches pending %s event for %d.", reason, spellId) pendingMatched = true tremove(self_pendingComboEvents, 1) end end if not pendingMatched and not OvaleFuture.inCombat and difference <= 0 then self:Debug(" Out-of-combat combo point decay.") if difference == 0 then -- Decrement the combo point count until game state catches up with the event. local newCombo = self.combo - 1 self.combo = newCombo > 0 and newCombo or 0 self:Debug(" Decaying to %d combo point(s).", self.combo) end end end end function OvaleComboPoints:Ovale_SpellFinished(event, atTime, spellId, targetGUID, success) self:Debug("%s (%f): Spell %d finished (%s) on %s", event, atTime, spellId, success, targetGUID or UNKNOWN) local si = OvaleData.spellInfo[spellId] if si and si.combo == "finisher" and (success == "hit" or success == "critical") then local target = OvaleGUID:GetUnitId(targetGUID) local combo = OvaleData:GetSpellInfoProperty(spellId, "combo", target) if combo == "finisher" then self:Debug(" Spell %d hit and consumed all combo points.", spellId) AddPendingComboEvent(atTime, spellId, targetGUID, "finisher", combo) if self_hasRuthlessness and self.combo == MAX_COMBO_POINTS then -- Ruthlessness grants a 20% chance to grant a combo point for each combo point spent on a finishing move. self:Debug(" Spell %d has 100% chance to grant an extra combo point from Ruthlessness.", spellId) AddPendingComboEvent(atTime, spellId, targetGUID, "Ruthlessness", 1) end if self_hasAnticipation and targetGUID ~= self_guid then -- Anticipation causes offensive finishing moves to consume all Anticipation charges and to grant a combo point for each. local unitId = OvaleGUID:GetUnitId(targetGUID) if unitId and API_UnitCanAttack("player", unitId) then local aura = OvaleAura:GetAuraByGUID(self_guid, ANTICIPATION, "HELPFUL", true) if OvaleAura:IsActiveAura(aura) then self:Debug(" Spell %d hit with %d Anticipation charges.", spellId, aura.stacks) AddPendingComboEvent(atTime, spellId, targetGUID, "Anticipation", aura.stacks) end end end end end end function OvaleComboPoints:Ovale_TalentsChanged(event) if self_class == "ROGUE" then self_hasAnticipation = OvaleSpellBook:GetTalentPoints(ANTICIPATION_TALENT) > 0 self_hasRuthlessness = OvaleSpellBook:IsKnownSpell(RUTHLESSNESS) end end function OvaleComboPoints:Update() self:StartProfiling("OvaleComboPoints_Update") self.combo = API_UnitPower("player", 4) self:StopProfiling("OvaleComboPoints_Update") end function OvaleComboPoints:GetComboPoints() -- Remove expired events. local now = API_GetTime() RemovePendingComboEvents(now) -- Start with the true combo point total and adjust for any pending combo points. local total = self.combo for k = 1, #self_pendingComboEvents do local combo = self_pendingComboEvents[k].combo if combo == "finisher" then total = 0 else -- if type(combo) == "number" then total = total + combo end -- Clamp combo points to the maximum. if total > MAX_COMBO_POINTS then total = MAX_COMBO_POINTS end end return total end function OvaleComboPoints:DebugComboPoints() self:Print("Player has %d combo points.", self.combo) end -- Return the number of combo points required to cast the given spell. -- NOTE: Mirrored in statePrototype below. function OvaleComboPoints:ComboPointCost(spellId, target) OvaleComboPoints:StartProfiling("OvaleComboPoints_ComboPointCost") local spellCost = 0 local si = OvaleData.spellInfo[spellId] if si and si.combo then local cost if self.GetSpellInfoProperty then cost = self.GetSpellInfoProperty(self, spellId, "combo", target) else cost = OvaleData:GetSpellInfoProperty(spellId, "combo", target) end --[[ combo == 0 means the that spell uses no resources. combo > 0 means that the spell generates combo points. combo < 0 means that the spell costs combo points. combo == "finisher" means that the spell uses all of the combo points (zeroes it out). --]] if cost == "finisher" then -- This spell is a finisher so compute the cost based on the amount of resources consumed. cost = self:GetComboPoints() -- Clamp cost between values defined by min_combo and max_combo. local minCost = si.min_combo or si.mincombo or 1 local maxCost = si.max_combo if cost < minCost then cost = minCost end if maxCost and cost > maxCost then cost = maxCost end else --[[ Add extra combo points generated by presence of a buff. "buff_combo" is the spell ID of the buff that causes extra resources to be generated or used. "buff_combo_amount" is the amount of extra resources generated or used, defaulting to 1 (one extra combo point generated). --]] local buffExtra = si.buff_combo if buffExtra then local isActiveAura -- Check for inherited/mirrored method first (for statePrototype). if self.GetAura then local aura = self.GetAura(self, "player", buffExtra, nil, true) isActiveAura = self.IsActiveAura(self, aura) else local aura = OvaleAura:GetAura("player", buffExtra, nil, true) isActiveAura = OvaleAura:IsActiveAura(aura) end if isActiveAura then local buffAmount = si.buff_combo_amount or 1 cost = cost + buffAmount end end cost = -1 * cost end spellCost = cost end OvaleComboPoints:StopProfiling("OvaleComboPoints_ComboPointCost") return spellCost end -- Run-time check that the player has enough combo points. -- NOTE: Mirrored in statePrototype below. function OvaleComboPoints:RequireComboPointsHandler(spellId, requirement, tokenIterator, target) local verified = false local cost = tokenIterator() if cost then cost = self:ComboPointCost(spellId, target) if cost > 0 then local power = self:GetComboPoints() if power >= cost then result = "passed" verified = true end else verified = true end if cost > 0 then local result = verified and "passed" or "FAILED" self:Log(" Require %d combo point(s): %s", cost, result) end else Ovale:OneTimeMessage("Warning: requirement '%s' is missing a cost argument.", requirement) end return verified, requirement end --</public-static-methods> --[[---------------------------------------------------------------------------- State machine for simulator. --]]---------------------------------------------------------------------------- --<public-static-properties> OvaleComboPoints.statePrototype = {} --</public-static-properties> --<private-static-properties> local statePrototype = OvaleComboPoints.statePrototype --</private-static-properties> --<state-properties> statePrototype.combo = nil --</state-properties> --<public-static-methods> -- Initialize the state. function OvaleComboPoints:InitializeState(state) state.combo = 0 end -- Reset the state to the current conditions. function OvaleComboPoints:ResetState(state) self:StartProfiling("OvaleComboPoints_ResetState") state.combo = self:GetComboPoints() -- Scan the pending combo point events and remove the Anticipation buff if there is pending Anticipation event. for k = 1, #self_pendingComboEvents do local comboEvent = self_pendingComboEvents[k] if comboEvent.reason == "Anticipation" then state:RemoveAuraOnGUID(self_guid, ANTICIPATION, "HELPFUL", true, comboEvent.atTime) break end end self:StopProfiling("OvaleComboPoints_ResetState") end -- Apply the effects of the spell on the player's state, assuming the spellcast completes. function OvaleComboPoints:ApplySpellAfterCast(state, spellId, targetGUID, startCast, endCast, isChanneled, spellcast) self:StartProfiling("OvaleComboPoints_ApplySpellAfterCast") local si = OvaleData.spellInfo[spellId] if si and si.combo then local target = OvaleGUID:GetUnitId(targetGUID) local cost = state:ComboPointCost(spellId, target) local power = state.combo power = power - cost -- Clamp combo points to lower and upper limits. if power <= 0 then power = 0 -- Ruthlessness grants a 20% chance to grant a combo point for each combo point spent on a finishing move. if self_hasRuthlessness and self.combo == MAX_COMBO_POINTS then state:Log("Spell %d grants one extra combo point from Ruthlessness.", spellId) power = power + 1 end -- Anticipation causes offensive finishing moves to consume all Anticipation charges and to grant a combo point for each. if self_hasAnticipation and state.combo > 0 then local aura = state:GetAuraByGUID(self_guid, ANTICIPATION, "HELPFUL", true) if state:IsActiveAura(aura, endCast) then power = power + aura.stacks state:RemoveAuraOnGUID(self_guid, ANTICIPATION, "HELPFUL", true, state.currentTime) -- Anticipation charges that are consumed to grant combo points don't overflow into new Anticipation charges. if power > MAX_COMBO_POINTS then power = MAX_COMBO_POINTS end end end end if power > MAX_COMBO_POINTS then --[[ If a rogue is talented into Anticipation, then any combo points over MAX_COMBO_POINTS are added to the Anticipation charges on the player to a maximum of MAX_COMBO_POINTS charges. If a spell is flagged with "temp_combo=1", then any combo points it grants cannot overflow into Anticipation charges. --]] if self_hasAnticipation and not si.temp_combo then local stacks = power - MAX_COMBO_POINTS -- Look for a pre-existing Anticipation buff and add to its stack count. local aura = state:GetAuraByGUID(self_guid, ANTICIPATION, "HELPFUL", true) if state:IsActiveAura(aura, endCast) then stacks = stacks + aura.stacks if stacks > MAX_COMBO_POINTS then stacks = MAX_COMBO_POINTS end end -- Add a new Anticipation buff with the updated start, ending, stacks information. local start = state.currentTime local ending = start + ANTICIPATION_DURATION aura = state:AddAuraToGUID(self_guid, ANTICIPATION, self_guid, "HELPFUL", start, ending) aura.stacks = stacks end power = MAX_COMBO_POINTS end state.combo = power end self:StopProfiling("OvaleComboPoints_ApplySpellAfterCast") end --</public-static-methods> --<state-methods> statePrototype.GetComboPoints = function(state) return state.combo end -- Mirrored methods. statePrototype.ComboPointCost = OvaleComboPoints.ComboPointCost statePrototype.RequireComboPointsHandler = OvaleComboPoints.RequireComboPointsHandler --</state-methods>