-- This file is part of Raid DPS -- -- (C) 2016 Scott Yeskie (Sasky) -- -- This program is free software; you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation; either version 2 of the License, or -- (at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see <http://www.gnu.org/licenses/>. -- TODO: Fix bosses removed > 0hp -- TODO: Clear on player activate? --- -- Raid DPS -- -- Calculates the raid DPS by examining health changes on the boss(es) -- This doesn't account for adds (as there isn't an easy way to account for them) -- but still provides an effective measure (provided the group kills the boss). -- -- One thing to note is TOTAL HEALTH is used here as the {starting health} + {healing to boss} -- * Starting health is used incase the addon in initialized mid-fight (or a boss isn't initially picked up) -- It should be the same as max health 99%+ of the time. -- * Boss healing is added in as it represents additional damage needed to take off that can be accounted for. -- Note: because of this, on some bosses (eg: 3 flesh atronauchs) a higher dps might be a longer time -- -- Design: There are two different update loops: the model/data update and the UI update. The model is updated on -- the EVENT_POWER_UPDATE, but the UI is moderated to only update every second to lessen addon impact -- local RaidDPS = ZO_Object:Subclass() function RaidDPS:New(...) local ctl = ZO_Object.New(self) ctl:Init(...) return ctl end --- -- @desc Main initialization function -- function RaidDPS:Init() self:ResetCounters() self.inFight = false local control = CreateTopLevelWindow("RaidDPS_UI") control:RegisterForEvent(EVENT_BOSSES_CHANGED, function(a,b,c,d) self:CheckAllBosses(a,b,c,d) end) control:RegisterForEvent(EVENT_POWER_UPDATE, function(_, unitTag, _, _, powerValue) self:OnPowerUpdate(unitTag, powerValue) end) control:AddFilterForEvent(EVENT_POWER_UPDATE, REGISTER_FILTER_POWER_TYPE, POWERTYPE_HEALTH) control:AddFilterForEvent(EVENT_POWER_UPDATE, REGISTER_FILTER_UNIT_TAG_PREFIX, "boss") control:RegisterForEvent(EVENT_PLAYER_ACTIVATED, function() self:CreateUI() end) self.ui = self.ui or {} self.ui.main = control --TODO Check bosses on combat state change for end? end --- Add boss to be tracked -- @desc This is used to initiate tracking for a boss. For each boss, last value and effective max are stored -- @param unitTag - boss to register health values for -- function RaidDPS:AddBoss(unitTag) local bossEntry = {} local health, max = GetUnitPower(unitTag, POWERTYPE_HEALTH) bossEntry.current = health --Max is used for determining resets bossEntry.max = max --Entered mid-fight, so go from current health for total health bossEntry.total = health if not self.bossHealth[unitTag] or not self.inFight then --Update totals self.allBossesCurrent = self.allBossesCurrent + health self.allBossesMax = self.allBossesMax + max self.allBossesTotal = self.allBossesTotal + health end self.bossHealth[unitTag] = bossEntry end --TODO: Tie UI update to start/stop function RaidDPS:StartFight() local now = GetGameTimeMilliseconds() if self.allBossesTotal > self.allBossesMax then self:CheckAllBosses() end self.inFight = true self.fightStart = now self.fightEnd = false EVENT_MANAGER:RegisterForUpdate("RaidDPS_UI_Update", 1000, function() self:UpdateUI() end) end function RaidDPS:EndFight() if self.inFight then self.fightEnd = GetGameTimeMilliseconds() self:UpdateUI() self.inFight = false EVENT_MANAGER:UnregisterForUpdate("RaidDPS_UI_Update") end end --- -- @desc Runs through all boss tags and adds those that exist -- The update event doesn't have anything, so it needs to run -- function RaidDPS:CheckAllBosses() self.ui.main:SetHidden(false) if not self.inFight then self:ResetCounters() end local nBosses = 0 for i = 1, MAX_BOSSES do if DoesUnitExist("boss"..i) then nBosses = nBosses + 1 self:AddBoss("boss"..i) end end if self.inFight then if nBosses == 0 then self:EndFight() end else if nBosses ~= 0 and self.allBossesMax ~= self.allBossesCurrent and self.allBossesCurrent ~= 0 then --d("Mid-fight start") self:StartFight() end end --d("Bosses changed: " .. nBosses .. " bosses") end function RaidDPS:ResetCounters() self.bossHealth = {} self.fightStart = 0 self.fightEnd = 1 self.allBossesCurrent = 0 self.allBossesMax = 0 self.allBossesTotal = 0 -- Zero-indexing totals so we can use modulo self.totals = {} for i=0,9 do self.totals[i] = 0 end self.nextTotal = 0 end --- Utility function for -- @return1 current health of all bosses -- @return2 current effective total of all bosses function RaidDPS:GetDamage() return self.allBossesTotal - self.allBossesCurrent end --- Main handler for the EVENT_POWER_UPDATE -- @param unitTag - boss that has health value changing -- @param powerValue - new health value for that boss -- function RaidDPS:OnPowerUpdate(unitTag, powerValue) if(self.bossHealth[unitTag]) then --Start fight if not self.inFight then self:StartFight() end local bossEntry = self.bossHealth[unitTag] local oldHP = bossEntry.current local deltaPower = powerValue - oldHP --d("UPDATE("..unitTag.."): "..oldHP.." -> "..powerValue) --Update current bossEntry.current = powerValue self.allBossesCurrent = self.allBossesCurrent + deltaPower --Increase cap if healing done to boss if deltaPower > 0 then bossEntry.total = bossEntry.total + deltaPower self.allBossesTotal = self.allBossesTotal + deltaPower if self.allBossesCurrent == self.allBossesMax then self:EndFight() --Raid wipe and boss reset end end --End fight if self.allBossesCurrent == 0 then --Out of boss range self:EndFight() end end end function RaidDPS:CreateUI() --Load settings self.cfg = ZO_SavedVars:NewAccountWide("RaidDPS_SavedVars", 1.0, "config", {}) local cfg = self.cfg local font = "EsoUI/Common/Fonts/univers67.otf|16|thin" local ui = self.ui ui.main = ui.main or WINDOW_MANAGER:CreateTopLevelWindow("RaidDPS_UI") ui.main:SetDimensions(280, 30) ui.main:SetMouseEnabled(true) ui.main:SetMovable(true) ui.main:SetClampedToScreen(true) ui.main:SetHandler("OnMoveStop", function(window) self:SaveUIPosition(window) end) ui.main:ClearAnchors() ui.bg = WINDOW_MANAGER:CreateControl("RaidDPS_UI_bg", RaidDPS_UI, CT_BACKDROP) ui.bg:SetDimensions(280, 30) ui.bg:SetCenterColor(0, 0, 0, 0.3) ui.bg:SetEdgeColor(0, 0, 0, 0) ui.txtFullDps = WINDOW_MANAGER:CreateControl("RaidDPS_UI_dpsFull", ui.bg, CT_LABEL) ui.txtDps10 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_dps10", ui.bg, CT_LABEL) ui.txtTime = WINDOW_MANAGER:CreateControl("RaidDPS_UI_time", ui.bg, CT_LABEL) ui.txtTime10 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_time10", ui.bg, CT_LABEL) ui.txtDps10:SetFont(font) ui.txtFullDps:SetFont(font) ui.txtTime:SetFont(font) ui.txtTime10:SetFont(font) -- ui.imgTimer1 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_clock1", ui.bg, CT_TEXTURE) ui.imgTimer1:SetTexture("/esoui/art/miscellaneous/timer_32.dds") ui.imgTimer1:SetDimensions(16, 16) ui.imgTimer2 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_clock2", ui.bg, CT_TEXTURE) ui.imgTimer2:SetTexture("/esoui/art/miscellaneous/timer_32.dds") ui.imgTimer2:SetDimensions(16, 16) ui.imgFull = WINDOW_MANAGER:CreateControl("RaidDPS_UI_iconFull", ui.bg, CT_TEXTURE) ui.imgFull:SetTexture("/esoui/art/trials/trialpoints_high.dds") ui.imgFull:SetDimensions(20, 20) ui.img10 = WINDOW_MANAGER:CreateControl("RaidDPS_UI_icon10", ui.bg, CT_TEXTURE) ui.img10:SetTexture("/esoui/art/trials/trialpoints_low.dds") ui.img10:SetDimensions(20, 20) ui.main:SetAnchor( cfg.selfPoint or TOPLEFT, GuiRoot, cfg.anchPoint or TOPLEFT, cfg.xoff or 10, cfg.yoff or 50 ) ui.bg:SetAnchor(TOPLEFT, RaidDPS_UI, TOPLEFT, 0, yoff) ui.imgFull:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 5, 5) ui.txtFullDps:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 30, 6) ui.imgTimer1:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 80, 9) ui.txtTime:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 100, 6) ui.img10:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 155, 5) ui.txtDps10:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 180, 6) ui.imgTimer2:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 230, 9) ui.txtTime10:SetAnchor(TOPLEFT, ui.main, TOPLEFT, 250, 6) ui.txtFullDps:SetText("0") ui.txtDps10:SetText("0") ui.txtTime:SetText("0:00") ui.txtTime10:SetText(":10") ui.main:SetHidden(true) ui.main:UnregisterForEvent(EVENT_PLAYER_ACTIVATED) ui.main:RegisterForEvent(EVENT_ACTION_LAYER_POPPED, function(_,_,indx) self:ActionLayerChange(indx) end) ui.main:RegisterForEvent(EVENT_ACTION_LAYER_PUSHED, function(_,_,indx) self:ActionLayerChange(indx) end) end function RaidDPS:ActionLayerChange(activeLayerIndex) self.ui.bg:SetHidden(activeLayerIndex > 2) end function RaidDPS:SaveUIPosition( window ) local _, sP, _, aP, x, y = window:GetAnchor() local cfg = self.cfg cfg.anchPoint = aP cfg.selfPoint = sP cfg.xoff = x cfg.yoff = y end function RaidDPS:UpdateUI() if not self.inFight then return end local curDamage = self:GetDamage() local ui = self.ui local endTime = self.fightEnd if self.inFight then endTime = GetGameTimeMilliseconds() end local fullFightTime = (endTime - self.fightStart) ui.txtTime:SetText(FormatTimeMilliseconds(fullFightTime, TIME_FORMAT_STYLE_COLONS, TIME_FORMAT_PRECISION_SECONDS, TIME_FORMAT_DIRECTION_ASCENDING)) local dps_total = curDamage * 1000 / fullFightTime ui.txtFullDps:SetText(FormatIntegerWithDigitGrouping(zo_round(dps_total), ",", 3)) if self.inFight then self.nextTotal = (self.nextTotal + 1) % 10 local dps_10sec = (curDamage - self.totals[self.nextTotal]) / 10 ui.txtDps10:SetText(FormatIntegerWithDigitGrouping(dps_10sec, ",", 3)) self.totals[self.nextTotal] = curDamage end end RAID_DPS = RaidDPS:New()