-- AddOn Namespace Definition RotationHero = {} RotationHero.name = "RotationHero" RotationHero.debugAbilities = false RotationHero.debugCombatState = false function RotationHero:Initialize() self.inCombat = IsUnitInCombat("player") self.playerName = GetRawUnitName("player") self.combatStartTime = 0 self.combatEventsLog = {} self.abilityBarSelected = "" self.statCombatTime = 0 self.statCombatLACount = 0 self.statCombatLAHit = 0 self.statCombatLACancelCount = 0 self.statCombatLACancelTime = 0 self.statCombatABCount = 0 self.statCombatABCancelCount = 0 self.statCombatABCancelTime = 0 self.statCombatBSCount = 0 self.statCombatULCount = 0 self.settingEfficencyOn = false -- call the handler to initialize the action slot values self.OnActionSlotsFullUpdate(nil, true) EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState) EVENT_MANAGER:RegisterForEvent(self.name, EVENT_COMBAT_EVENT, self.OnCombatEvent) EVENT_MANAGER:RegisterForEvent(self.name, EVENT_ACTION_SLOTS_FULL_UPDATE, self.OnActionSlotsFullUpdate) EVENT_MANAGER:RegisterForEvent(self.name, EVENT_ACTION_SLOT_ABILITY_USED, self.OnActionSlotAbilityUsed) SLASH_COMMANDS["/rh"] = RotationHero.SlashCommand end function RotationHero.OnAddOnLoaded(eventCode, addonName) if addonName == RotationHero.name then EVENT_MANAGER:UnregisterForEvent(addonName, eventCode) RotationHero:Initialize() end end EVENT_MANAGER:RegisterForEvent(RotationHero.name, EVENT_ADD_ON_LOADED, RotationHero.OnAddOnLoaded) function RotationHero.SlashCommand(extra) local options = {} local searchResult = { string.match(extra,"^(%S*)%s*(.-)$") } for i,v in pairs(searchResult) do if (v ~= nil and v ~= "") then options[i] = string.lower(v) end end if #options == 0 or options[1] == "help" then d("Usage of /rh:") d(" help - display this help") d(" eff - show combat efficiency") d(" eff on/off - toggle automatic display after combat") d(" print - show animation cancelling stats") elseif options[1] == "eff" then if options[2] == "on" then RotationHero.settingEfficencyOn = true d("Combat Efficiency will be displayed after each combat") elseif options[2] == "off" then RotationHero.settingEfficencyOn = false d("Combat Efficiency will not be displayed after combat") else RotationHero.PrintEfficency() end elseif options[1] == "print" then RotationHero.PrintStats() end end -- handle change of combat state (in combat, or out of combat) function RotationHero.OnPlayerCombatState(event, inCombat) if inCombat ~= RotationHero.inCombat then local nowTime = GetGameTimeMilliseconds() if inCombat then if RotationHero.debugCombatState then d(RotationHero.LogCombatTime(nowTime) .. "Entering combat.") end -- we set the inCombat state and record the timestamp when combat started RotationHero.inCombat = true RotationHero.combatStartTime = nowTime RotationHero.AddCombatEvent(nowTime, { type = "start" }) else if RotationHero.debugCombatState then d(RotationHero.LogCombatTime(nowTime) .. "Exiting combat.") end RotationHero.AddCombatEvent(nowTime, { type = "end" }) -- set the inCombat to false RotationHero.inCombat = false RotationHero.CalculateStats() if RotationHero.settingEfficencyOn then RotationHero.PrintEfficency() end -- reset the combat dependant vars RotationHero.combatStartTime = 0 RotationHero.combatEventsLog = {} end end end function RotationHero.OnCombatEvent(eventID, result, isError, abilityName, abilityGraphic, abilityActionSlotType, sourceName, sourceType, targetName, targetType, hitValue, powerType, damageType, log, sourceUnitId, targetUnitId, abilityId) local nowTime = GetGameTimeMilliseconds() --[[ if RotationHero.debug then d("result=" .. tostring(result) .. ", isError=" .. tostring(isError) .. ", abilityName=" .. tostring(abilityName) .. ", abilityGraphic=" .. tostring(abilityGraphic) .. ", abilityActionSlotType=" .. tostring(abilityActionSlotType) .. ", sourceName=" .. tostring(sourceName) .. ", sourceType=" .. tostring(sourceType) .. ", targetName=" .. tostring(targetName) .. ", targetType=" .. tostring(targetType) .. ", hitValue=" .. tostring(hitValue) .. ", powerType=" .. tostring(powerType) .. ", damageType=" .. tostring(damageType) .. ", log=" .. tostring(log) .. ", sourceUnitId=" .. tostring(sourceUnitId) .. ", targetUnitId=" .. tostring(targetUnitId) .. ", abilityId=" .. tostring(abilityId)) end]] if sourceName == RotationHero.playerName then if abilityActionSlotType == ACTION_SLOT_TYPE_LIGHT_ATTACK then if result == 1 or result == 2 then RotationHero.AddCombatEvent(nowTime, { type = "damage", name = abilityName, ability = ACTION_SLOT_TYPE_LIGHT_ATTACK, critical = (result == 2 and true or false), damage = hitValue }) end end end end -- called anytime action slots change and on bar swap function RotationHero.OnActionSlotsFullUpdate(eventID, isBarSwap) if isBarSwap then local activeWeaponPair, locked = GetActiveWeaponPairInfo() -- should be one of: ACTIVE_WEAPON_PAIR_MAIN, ACTIVE_WEAPON_PAIR_BACKUP, ACTIVE_WEAPON_PAIR_NONE local abilityBar = activeWeaponPair == ACTIVE_WEAPON_PAIR_MAIN and "front" or "back" if abilityBar ~= RotationHero.abilityBarSelected then local nowTime = GetGameTimeMilliseconds() if RotationHero.abilityBarSelected ~= "" then if RotationHero.debugAbilities then d(RotationHero.LogCombatTime(nowTime) .. "Bar swap: " .. RotationHero.abilityBarSelected .. " -> " .. abilityBar) end end RotationHero.AddCombatEvent(nowTime, {type = "barswap", bar = abilityBar}) RotationHero.abilityBarSelected = abilityBar end end end -- player has activated an ability function RotationHero.OnActionSlotAbilityUsed(eventId, abilitySlot) -- get the name of activated ability local abilityName = GetSlotName(abilitySlot) -- get the current time, so we can calculate offsets local nowTime = GetGameTimeMilliseconds() if RotationHero.debugAbilities then d(RotationHero.LogCombatTime(nowTime) .. abilityName) end RotationHero.AddCombatEvent(nowTime, { type = "ability", slot = abilitySlot, name = abilityName }) end function RotationHero.LogCombatTime(time) if RotationHero.combatStartTime == 0 or RotationHero.combatStartTime > time then return "[0] " end return "[" .. tostring(time - RotationHero.combatStartTime) .. "] " end function RotationHero.AddCombatEvent(time, event) RotationHero.combatEventsLog[time] = event end function RotationHero.CalculateStats() local timestamps = {} for index, value in pairs(RotationHero.combatEventsLog) do table.insert(timestamps, index) end table.sort(timestamps) -- find the combat start; we can count anything cast up to 1s before the start of combat to belong aswell local startParsingFromIdx = 0 local timestampIdx = 1 while timestamps[timestampIdx] ~= nil and timestamps[timestampIdx] < (RotationHero.combatStartTime - 1000) do timestampIdx = timestampIdx + 1 end -- search until the actual start of combat event, and see if there is an end of combat event, then adjust startParsingFromIndex = timestampIdx while timestamps[timestampIdx] ~= nil and timestamps[timestampIdx] < RotationHero.combatStartTime do if RotationHero.combatEventsLog[timestamps[timestampIdx]].type == "end" then startParsingFromIndex = timestampIdx + 1 end timestampIdx = timestampIdx + 1 end -- initialize stats to zero RotationHero.statCombatTime = 0 RotationHero.statCombatLACount = 0 RotationHero.statCombatLAHit = 0 RotationHero.statCombatLACancelCount = 0 RotationHero.statCombatLACancelTime = 0 RotationHero.statCombatABCount = 0 RotationHero.statCombatABCancelCount = 0 RotationHero.statCombatABCancelTime = 0 RotationHero.statCombatBSCount = 0 RotationHero.statCombatULCount = 0 -- some state handling variables local sLAFired = 0 local sABFired = 0 local sLastTime = 0 -- ok now we parse until the end of combat timestampIdx = startParsingFromIndex while timestamps[timestampIdx] do local timestamp = timestamps[timestampIdx] local event = RotationHero.combatEventsLog[timestamp] if event.type == "ability" then sLastTime = timestamp if event.slot == 1 then RotationHero.statCombatLACount = RotationHero.statCombatLACount + 1 sLAFired = timestamp sABFired = 0 elseif event.slot > 2 and event.slot < 8 then if sLAFired > 0 and timestamp < sLAFired + 1000 then RotationHero.statCombatLACancelCount = RotationHero.statCombatLACancelCount + 1 RotationHero.statCombatLACancelTime = RotationHero.statCombatLACancelTime + (timestamp - sLAFired) end RotationHero.statCombatABCount = RotationHero.statCombatABCount + 1 sABFired = timestamp elseif event.slot == 8 then RotationHero.statCombatULCount = RotationHero.statCombatULCount + 1 end elseif event.type == "damage" then if event.ability == ACTION_SLOT_TYPE_LIGHT_ATTACK then RotationHero.statCombatLAHit = RotationHero.statCombatLAHit + 1 sLAFired = 0 end elseif event.type == "barswap" then RotationHero.statCombatBSCount = RotationHero.statCombatBSCount + 1 if sABFired > 0 and timestamp < sABFired + 1000 then RotationHero.statCombatABCancelCount = RotationHero.statCombatABCancelCount + 1 RotationHero.statCombatABCancelTime = RotationHero.statCombatABCancelTime + (timestamp - sABFired) end end timestampIdx = timestampIdx + 1 end RotationHero.statCombatTime = math.ceil((sLastTime - RotationHero.combatStartTime)/1000) end function RotationHero.PrintEfficency() if (RotationHero.statCombatLACount > 0 and RotationHero.statCombatTime > 0) then d("Combat Efficiency:" .. " LA hits " .. tostring(RotationHero.statCombatLAHit) .. "/" .. tostring(RotationHero.statCombatLACount) .. "(" .. tostring(math.floor(RotationHero.statCombatLAHit / RotationHero.statCombatLACount * 100.0)) .. "%)" .. " Abilities " .. tostring(RotationHero.statCombatABCount) .. "/" .. tostring(RotationHero.statCombatTime) .. "(" .. tostring(math.floor(RotationHero.statCombatABCount / RotationHero.statCombatTime * 100.0)) .. "%)") end end function RotationHero.PrintStats() if RotationHero.statCombatLACount > 0 then d("Light Attack Canceling: " .. tostring(RotationHero.statCombatLACancelCount) .. "/" .. tostring(RotationHero.statCombatLACount) .. "(" .. tostring(math.floor(RotationHero.statCombatLACancelCount / RotationHero.statCombatLACount * 100.0)) .. "%)" .. " [" .. tostring(RotationHero.statCombatLACancelCount > 0 and math.floor(RotationHero.statCombatLACancelTime / RotationHero.statCombatLACancelCount) or 0) .. "ms]") end if RotationHero.statCombatBSCount > 0 then d("Ability Canceling: " .. tostring(RotationHero.statCombatABCancelCount) .. "/" .. tostring(RotationHero.statCombatBSCount) .. "(" .. tostring(math.floor(RotationHero.statCombatABCancelCount / RotationHero.statCombatBSCount * 100.0)) .. "%)" .. " [" .. tostring(RotationHero.statCombatABCancelCount > 0 and math.floor(RotationHero.statCombatABCancelTime / RotationHero.statCombatABCancelCount) or 0) .. "ms]") end end function RotationHero.DumpTable(o) if type(o) == 'table' then local s = '{ ' for k,v in pairs(o) do if type(k) ~= 'number' then k = '"'..k..'"' end s = s .. k .. ' : ' .. RotationHero.DumpTable(v) .. ',' end return s .. '} ' else if type(o) ~= 'number' then return '"' .. tostring(o) .. '"' else return tostring(o) end end end