-- $Id$ --[[ LICENSE Copyright (c) 2006-2007, Kyle Smith All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --]] --[[ DOCUMENTATION --]] local name = "IncomingHealBars" local version = tonumber(("$Revision$"):match("(%d+)")) or 1 local oldLib = getglobal(name) if oldLib and oldLib.version >= version then return end local lib = {} lib.name = name lib.version = version --[[ LIBRARIES ]]------------------------------------------------------------- local CastCommLib = CastCommLib --[[ LOCALIZATION ]]---------------------------------------------------------- local L local currentLocale = GetLocale() local L_defaults = { } if currentLocale == "enUS" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "Lesser Heal", SPELL_HEAL = "Heal", SPELL_FLASH_HEAL = "Flash Heal", SPELL_GREATER_HEAL = "Greater Heal", SPELL_PRAYER_OF_HEALING = "Prayer of Healing", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "Holy Light", SPELL_FLASH_OF_LIGHT = "Flash of Light", -- druid heal spells with a casting time SPELL_REGROWTH = "Regrowth", SPELL_HEALING_TOUCH = "Healing Touch", -- shaman heal spells with a casting time SPELL_LESSER_HEALING_WAVE = "Lesser Healing Wave", SPELL_HEALING_WAVE = "Healing Wave", } elseif currentLocale == "deDE" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "Geringes Heilen", SPELL_HEAL = "Heilen", SPELL_FLASH_HEAL = "Blitzheilung", SPELL_GREATER_HEAL = "Gro\195\159e Heilung", SPELL_PRAYER_OF_HEALING = "Gebet der Heilung", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "Heiliges Licht", SPELL_FLASH_OF_LIGHT = "Lichtblitz", -- druid heal spells with a casting time SPELL_REGROWTH = "Nachwachsen", SPELL_HEALING_TOUCH = "Heilende Ber\195\188hrung", -- shaman heal spells with a casting time SPELL_LESSER_HEALING_WAVE = "Geringe Welle der Heilung", SPELL_HEALING_WAVE = "Welle der Heilung", } elseif currentLocale == "frFR" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "Soins inf\195\169rieurs", SPELL_HEAL = "Soins", SPELL_FLASH_HEAL = "Soins rapides", SPELL_GREATER_HEAL = "Soins sup\195\169rieurs", SPELL_PRAYER_OF_HEALING = "Pri\195\168re de soins", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "Lumi\195\168re sacr\195\169e", SPELL_FLASH_OF_LIGHT = "Eclair lumineux", -- druid heal spells with a casting time SPELL_REGROWTH = "R\195\169tablissement", SPELL_HEALING_TOUCH = "Toucher gu\195\169risseur", -- shaman heal spells with a casting time SPELL_LESSER_HEALING_WAVE = "Vague de soins inf\195\169rieurs", SPELL_HEALING_WAVE = "Vague de soins", } elseif currentLocale == "zhCN" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "次级治疗术", SPELL_HEAL = "治疗术", SPELL_FLASH_HEAL = "快速治疗", SPELL_GREATER_HEAL = "强效治疗术", SPELL_PRAYER_OF_HEALING = "治疗祷言", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "圣光术", SPELL_FLASH_OF_LIGHT = "圣光闪现", -- druid heal spells with a casting time SPELL_REGROWTH = "愈合", SPELL_HEALING_TOUCH = "治疗之触", -- shaman heal spells with a casting time SPELL_LESSER_HEALING_WAVE = "次级治疗波", SPELL_HEALING_WAVE = "治疗波", } elseif currentLocale == "zhTW" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "次級治療術", SPELL_HEAL = "治療術", SPELL_FLASH_HEAL = "快速治療", SPELL_GREATER_HEAL = "強效治療術", SPELL_PRAYER_OF_HEALING = "治療禱言", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "聖光術", SPELL_FLASH_OF_LIGHT = "聖光閃現", -- druid heal spells with a casting time SPELL_REGROWTH = "癒合", SPELL_HEALING_TOUCH = "治療之觸", -- shaman heal spells with a casting time SPELL_LESSER_HEALING_WAVE = "次級治療波", SPELL_HEALING_WAVE = "治療波", } elseif currentLocale == "koKR" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "하급 치유", SPELL_HEAL = "치유", SPELL_FLASH_HEAL = "순간 치유", SPELL_GREATER_HEAL = "상급 치유", SPELL_PRAYER_OF_HEALING = "치유의 기원", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "성스러운 빛", SPELL_FLASH_OF_LIGHT = "빛의 섬광", -- druid heal spells with a casting time SPELL_REGROWTH = "재생", SPELL_HEALING_TOUCH = "치유의 손길", -- shaman heal spells with a casting time SPELL_LESSER_HEALING_WAVE = "하급 치유의 물결", SPELL_HEALING_WAVE = "치유의 물결", } elseif currentLocale == "esES" then L = { -- priest heal spells with a casting time SPELL_LESSER_HEAL = "Sanación inferior", SPELL_HEAL = "Sanación", SPELL_FLASH_HEAL = "Sanación instantánea", SPELL_GREATER_HEAL = "Sanación superior", SPELL_PRAYER_OF_HEALING = "Rezo de sanación", -- paladin heal spells with a casting time SPELL_HOLY_LIGHT = "Luz Sagrada", SPELL_FLASH_OF_LIGHT = "Luz Cegadora", -- druid heal spells with a casting time SPELL_REGROWTH = "Recrecimiento", SPELL_HEALING_TOUCH = "Toque de sanación", -- shaman heal spells with a casting time -- FIXME: confirm lesser healing wave SPELL_LESSER_HEALING_WAVE = "Ola de sanación inferior", SPELL_HEALING_WAVE = "Ola de sanación", } else -- TODO: steal translations from BabbleSpell error(("Locale %s not supported."):format(currentLocale)) end -- provide default translations setmetatable(L, { __index = L_defaults }) --[[ DATA TABLES ]]----------------------------------------------------------- -- spells to watch for local watchSpells = { -- priest [L["SPELL_LESSER_HEAL"]] = true, [L["SPELL_HEAL"]] = true, [L["SPELL_FLASH_HEAL"]] = true, [L["SPELL_GREATER_HEAL"]] = true, [L["SPELL_PRAYER_OF_HEALING"]] = true, -- paladin [L["SPELL_HOLY_LIGHT"]] = true, [L["SPELL_FLASH_OF_LIGHT"]] = true, -- druid [L["SPELL_REGROWTH"]] = true, [L["SPELL_HEALING_TOUCH"]] = true, -- shaman [L["SPELL_LESSER_HEALING_WAVE"]] = true, [L["SPELL_HEALING_WAVE"]] = true, } --[[ HERE'S THE REAL CODE ]]-------------------------------------------------- function lib:Initialize (old) self:ToggleDebugging(true) self:Debug("Initialize()") --self:CreateFrames() end function lib:Enable () self:Debug("Enable()") CastCommLib:RegisterCallback(self, "CastCommCallback") self.eventFrame:SetScript("OnUpdate", self.OnUpdate) end function lib:Disable () self:Debug("Disable()") self.eventFrame:SetScript("OnUpdate", nil) CastCommLib:UnregisterCallback(self, "CastCommCallback") self:UnregisterAllEvents() end local next_update = 0 function lib.OnUpdate (frame, elapsed) next_update = next_update - elapsed if next_update > 0 then return end lib:UpdateBars() -- change this back to 0.05 when we have casting bars working again --next_update = 0.05 next_update = 0.25 end --[[ HEAL TRACKING ]]--------------------------------------------------------- -- we're leaving all the data out here until we decide what does and doesn't -- need access to it -- heal data, indexed by caster local heal_startTime = {} local heal_endTime = {} local heal_target = {} -- heal data, indexed by target name -- track 5 heals for display per target [i][target] = caster local target_healer = { {}, {}, {}, {}, {} } do function lib:CastCommCallback (sender, senderUnit, action, target, channel, spell, rank, displayName, icon, startTime, endTime, isTradeSkill) if not watchSpells[spell] then return end self:Debug(sender, action, spell, target, startTime, endTime, endTime - startTime) if action == "START" then self:HealStart(sender, target, startTime, endTime) elseif action == "DELAYED" then self:HealDelayed(sender, target, startTime, endTime) elseif action == "INTERRUPTED" or action == "FAILED" or action == "SUCCEEDED" then self:HealStop(sender, target, startTime, GetTime()) end end function lib:HealStart(sender, target, startTime, endTime) heal_startTime[sender] = startTime/1000 heal_endTime[sender] = endTime/1000 heal_target[sender] = target self:UpdateSpells(target) end function lib:HealDelayed(sender, target, startTime, endTime) -- TODO: sanity check? heal_startTime[sender] = startTime/1000 heal_endTime[sender] = endTime/1000 self:UpdateSpells(target) end function lib:HealStop(sender, target, startTime, endTime) -- TODO: sanity check? heal_startTime[sender] = nil heal_endTime[sender] = nil heal_target[sender] = nil self:UpdateSpells(target) end -- sort used by UpdateSpells local function sortByStopTime (a, b) return heal_endTime[a] < heal_endTime[b] end local spellsForTarget = {} function lib:UpdateSpells (target) self:Debug("UpdateSpells(", target, ")") for k in pairs(spellsForTarget) do spellsForTarget[k] = nil end local current_time = GetTime() for caster, caster_target in pairs(heal_target) do if caster_target == target then -- self:Debug(caster, "->", target, heal_endTime[caster], current_time) if heal_endTime[caster] > current_time then table.insert(spellsForTarget, caster) else -- spell expired heal_startTime[caster] = nil heal_endTime[caster] = nil heal_target[caster] = nil end end end table.sort(spellsForTarget, sortByStopTime) for i = 1,5 do self:Debug("Heal", i, "for", target, "from", spellsForTarget[i]) target_healer[i][target] = spellsForTarget[i] end -- I'm assuming we want this here. It could potentially go in -- :HealStop if spellsForTarget[1] then self:ShowBarsForUnit(target) else self:HideBarsForUnit(target) end end end --[[ FRAME MANAGEMENT ]]------------------------------------------------------ -- here's where we detect new frames that need bars attached, update existing -- bars, and recycle unused bars do -- bars in use, indexed by parent frame local usedBars = {} function lib:UpdateBars () local current_time = GetTime() for parent, bar in pairs(usedBars) do bar:SetValue(current_time) end end function lib:ShowBarsForUnit (name) local heal1_caster = target_healer[1][name] local heal2_caster, heal3_caster, heal4_caster, heal5_caster -- this might not be worth it in the long run if heal1_caster then heal2_caster = target_healer[2][name] if heal2_caster then heal3_caster = target_healer[3][name] if heal3_caster then heal4_caster = target_healer[4][name] if heal4_caster then heal5_caster = target_healer[5][name] end end end end local unit = CastCommLib:GetUnitId(name) local current_time = GetTime() local startTime = heal_startTime[heal1_caster] local endTime = heal_endTime[heal1_caster] local remainingTime = endTime - current_time self:Debug("Heal 1 on", name, "from", heal1_caster, "remaining:", remainingTime) self:Debug("Other heals on", name, ":", heal2_caster, heal3_caster, heal4_caster, heal5_caster) for parent, bar in pairs(usedBars) do if parent:GetAttribute("unit") == unit then bar:SetMinMaxValues(startTime, endTime) bar:SetValue(current_time) bar:Show() end end end function lib:HideBarsForUnit (name) for parent, bar in pairs(usedBars) do if parent:GetAttribute("unit") == unit then bar:Hide() end end end -- maybe some of this should be in ConfigureCastBar function lib:AttachBarToFrame (frame) local bar = self:AcquireCastBar() bar:SetParent(frame) bar:SetPoint("TOP", frame, "TOP") bar:SetWidth(frame:GetWidth()) bar:SetHeight(5) end end --[[ FRAME FUNCTIONS ]]------------------------------------------------------- -- I'm not sure we're quite at the point where we need a full-blown class for -- the heal bars, but let's keep things modular -- bars do local function CastBar_Reset () end local function CastBar_Shutdown () if (frame.target and unitHealBars[frame.target]) then unitHealBars[frame.target][frame] = nil end frame.target = nil frame:ClearAllPoints() end local backdrop = { bgFile = "Interface/Tooltips/UI-Tooltip-Background", tile = true, tileSize = 16, edgeSize = 16, insets = { left = 0, right = 0, top = 0, bottom = 0 } } local framesCreated = 0 function lib:CreateCastBar () framesCreate = framesCreated + 1 local barName = ("%s_%d"):format(self.name, framesCreated) local bar = CreateFrame("StatusBar", barName, UIParent) bar:SetStatusBarTexture("Interface\\Addons\\Grid\\gradient32x32") bar:SetBackdrop(backdrop) bar:SetBackdropColor(0, 0, 0, 0.8) bar:SetStatusBarColor(0, 1, 0, 0.8) bar:SetWidth(100) bar:SetHeight(5) bar:SetMinMaxValues(0,100) bar:SetValue(50) bar:Hide() bar.Reset = CastBar_Reset bar.Shutdown = CastBar_Shutdown -- don't do this, we have lib.OnUpdate for updating all the -- cast bars -- bar:SetScript('OnUpdate',_onUpdate) bar:SetScript('OnHide', CastBar_Shutdown) return bar end function lib:ConfigureCastBar () bar:SetPoint("CENTER", UIParent, "CENTER") end end --[[ FRAME POOL ]]------------------------------------------------------------ -- keeping track of in-use frames is probably best done elsewhere like in the -- FRAME MANAGEMENT section do local barPool = {} function lib:AcquireCastBar (...) local bar local unusedCount = #barPool if unusedCount > 0 then self:Debug("Recycling unused frame,", unusedCount, "left") bar = table.remove(barPool, unusedCount) bar:Reset() else self:Debug("Creating new frame") bar = self:CreateCastBar() end self:ConfigureCastBar(bar, ...) return bar end function lib:ReleaseCastBar (bar) bar:Shutdown() table.insert(barPool, bar) end local sparkPool = {} function lib:AcquireSpark (...) local spark local unusedCount = #sparkPool if unusedCount > 0 then self:Debug("Recycling unused spark,", unusedCount, "left") spark = table.remove(sparkPool, unusedCount) else self:Debug("Creating new spark") spark = self:CreateSpark() end self:ConfigureSpark(spark, ...) return spark end function lib:ReclaimSpark (spark) table.insert(sparkPool, spark) end end --[[ LIBRARY CORE ]]---------------------------------------------------------- -- everything below here is just basic library stuff -- inspired by Dongle and Ace2 --[[ DEBUGGING ]]------------------------------------------------------------- do function lib:InitializeDebugging (old) if old then self.debug = old.debug self.debugFrame = old.debugFrame else self.debug = false self.debugFrame = ChatFrame1 end end local debugArgs = {} function lib:DebugPrint (...) for i = #debugArgs, 1, -1 do debugArgs[i] = nil end table.insert(debugArgs, string.format("|cfffcc000||%s ||%d|||r", self.name, self.version)) for i = 1, select("#", ...) do table.insert(debugArgs, tostring(select(i, ...))) end local message = table.concat(debugArgs, " ") self.debugFrame:AddMessage(message) end function lib:ToggleDebugging (on) if on == nil then self.debug = not self.debug else self.debug = on end self:DebugPrint("Debugging", self.debug and "|cff00cf00enabled|r" or "|cffcf0000disabled|r") return self.debug end function lib:Debug (...) if not self.debug then return end return self:DebugPrint(...) end end --[[ EVENT HANDLING ]]-------------------------------------------------------- do -- event dispatch table local events = {} -- event dispatch function local function OnEvent (frame, event, ...) local handler = events[event] lib.event = event if handler and type(lib[handler]) == "function" then lib:Debug("|cffcccc00Event:|r", event, ...) return lib[handler](lib, ...) end end -- I'm sure we'll use this for a timer eventually local time_since_last_schedule = 0 local time_until_next_schedule local next_scheduled_function local function OnUpdate (frame, elapsed) -- nothing to do if not time_until_next_schedule then return end time_since_last_schedule = time_since_last_schedule + elapsed -- nothing to do yet if time_since_last_schedule < time_until_next_schedule then return end next_scheduled_function() -- reset time_since_last_schedule = 0 time_until_next_schedule = nil next_scheduled_function = nil end function lib:InitializeEvents (old) if old then old:UnregisterAllEvents() self.eventFrame = old.eventFrame self.eventFrame:SetScript("OnEvent", nil) self.eventFrame:SetScript("OnUpdate", nil) else local frameName = self.name .. "_EVENT_FRAME" self.eventFrame = CreateFrame("Frame", frameName) end self.eventFrame:SetScript("OnEvent", OnEvent) -- self.eventFrame:SetScript("OnUpdate", OnUpdate) end function lib:RegisterEvent (event, handler) events[event] = handler or event self.eventFrame:RegisterEvent(event) end function lib:UnregisterEvent(event) self.eventFrame:UnregisterEvent(event) events[event] = nil end function lib:UnregisterAllEvents () for event in pairs(events) do self:UnregisterEvent(event) end end function lib:TriggerEvent (event, ...) return OnEvent(nil, event, ...) end end --[[ LIBRARY MANAGEMENT ]]---------------------------------------------------- function lib:Install () local old = getglobal(self.name) if old then old:Disable() end -- debugging self:InitializeDebugging(old) -- event handling self:InitializeEvents(old) -- install the library in the global namespace setglobal(self.name, self) -- register important event handlers self:RegisterEvent("LIB_INSTALLED", "Initialize") self:RegisterEvent("PLAYER_LOGIN", "Enable") self:RegisterEvent("PLAYER_LOGOUT", "Disable") -- tell the library to do it's thing self:TriggerEvent("LIB_INSTALLED", old) end lib:Install()