From f44904495845b6859f2fab0f8c4ebbcb7a7f0b31 Mon Sep 17 00:00:00 2001
From: Cortes-Jeremy <56119078+Cortes-Jeremy@users.noreply.github.com>
Date: Fri, 30 Apr 2021 23:05:54 +0200
Subject: [PATCH 1/3] Trying to add predictive healing to arena units
---
Gladius/Gladius.lua | 161 +-
Gladius/Gladius.toc | 2 +-
Gladius/cooldownlist.lua | 4 +
Gladius/embeds.xml | 29 +-
Gladius/frame.lua | 62 +-
.../AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua | 739 +++--
.../Changelog-LibHealComm-4.0-v1.6.6.txt | 13 +
.../LibHealCommArena-4.0/ChatThrottleLib.lua | 503 +++
.../LibHealCommArena-4.0.lua | 2861 +++++++++++++++++
.../LibHealCommArena-4.0.toc | 14 +
.../LibHealCommArena-4.0.xml | 5 +
.../CallbackHandler-1.0.toc | 15 +
.../CallbackHandler-1.0.lua | 239 ++
.../CallbackHandler-1.0.xml | 4 +
.../CallbackHandler-1.0/LibStub/LibStub.lua | 30 +
.../libs/LibStub/LibStub.lua | 30 +
.../libs/LibStub/LibStub.toc | 13 +
Gladius/libs/LibStub/LibStub.lua | 13 +-
Gladius/localization/enUS.lua | 1 +
Gladius/options.lua | 27 +-
20 files changed, 4325 insertions(+), 440 deletions(-)
create mode 100644 Gladius/libs/LibHealCommArena-4.0/Changelog-LibHealComm-4.0-v1.6.6.txt
create mode 100644 Gladius/libs/LibHealCommArena-4.0/ChatThrottleLib.lua
create mode 100644 Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.lua
create mode 100644 Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.toc
create mode 100644 Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.xml
create mode 100644 Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0.toc
create mode 100644 Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua
create mode 100644 Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml
create mode 100644 Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/LibStub/LibStub.lua
create mode 100644 Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.lua
create mode 100644 Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.toc
diff --git a/Gladius/Gladius.lua b/Gladius/Gladius.lua
index e2fcb46..e6c6cb5 100644
--- a/Gladius/Gladius.lua
+++ b/Gladius/Gladius.lua
@@ -2,6 +2,7 @@ Gladius = LibStub("AceAddon-3.0"):NewAddon("Gladius", "AceEvent-3.0", "AceConsol
local L = LibStub("AceLocale-3.0"):GetLocale("Gladius", true)
local LSM = LibStub("LibSharedMedia-3.0")
local LCG = LibStub("LibCustomGlow-1.0")
+local LHC = LibStub("LibHealCommArena-4.0")
local LAM = LibStub:GetLibrary("AbsorbsMonitor-1.0", true)
local arenaUnits = {}
@@ -24,6 +25,14 @@ function Gladius:OnInitialize()
self.buttons = {}
self.currentBracket = nil
+ LHC.UnregisterAllCallbacks(Gladius);
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealStarted", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealUpdated", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealDelayed", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealStopped", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_ModifierChanged", "HealCommArena_Modified")
+ LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified")
+
-- Populate the arenaUnits table
for i=1, 5 do
arenaUnits["arena" .. i] = "playerUnit"
@@ -222,6 +231,22 @@ function Gladius:JoinedArena()
-- Special arena event
self:RegisterEvent("ARENA_OPPONENT_UPDATE")
+ -- HealComm Events
+ --[[ LHC.UnregisterAllCallbacks(Gladius);
+ LHC.UnregisterAllCallbacks(Gladius);
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealStarted", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealUpdated", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealDelayed", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_HealStopped", "HealCommArena_Heal_Update")
+ LHC.RegisterCallback(Gladius, "HealCommArena_ModifierChanged", "HealCommArena_Modified")
+ LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified")]]
+
+ -- HealComm Events
+ LAM.UnregisterAllCallbacks(Gladius);
+ LAM.RegisterCallback(Gladius, "EffectApplied");
+ LAM.RegisterCallback(Gladius, "EffectUpdated");
+ LAM.RegisterCallback(Gladius, "EffectRemoved");
+
-- Find out the current bracket size
for i=1, MAX_BATTLEFIELD_QUEUES do
local status, _, _, _, _, teamSize = GetBattlefieldStatus(i)
@@ -242,6 +267,7 @@ function Gladius:JoinedArena()
button = self:CreateButton(i)
self.buttons[unit] = button
self.buttons[pet] = button.pet
+ self.buttons[unit].unit = unit
end
button:Show()
@@ -324,7 +350,7 @@ function Gladius:UNIT_HEALTH(event, unit)
-- update absorb bar
if( db.absorbBar and (arenaUnits[unit] == "playerUnit" or (arenaUnits[unit] ~= "playerUnit" and db.showPets)) ) then
- Gladius:UpdateAbsorb(event, unit, button)
+ Gladius:UpdateAbsorbBar(event, unit, button)
end
-- update cutaway
@@ -457,7 +483,7 @@ function Gladius:UNIT_AURA(event, unit)
-- update absorb bar
if( db.absorbBar and (arenaUnits[unit] == "playerUnit" or (arenaUnits[unit] ~= "playerUnit" and db.showPets)) ) then
- Gladius:UpdateAbsorb(event, unit, button)
+ Gladius:UpdateAbsorbBar(event, unit, button)
end
local aura = button.auraFrame
@@ -617,7 +643,7 @@ function Gladius:UNIT_SPELLCAST_START(event, unit)
if(not button) then return end
local castBar = button.castBar
- if( db.castBarOnCast ) then
+ if (db.castBar and db.castBarOnCast) then
castBar:Show()
end
castBar.isCasting = true
@@ -651,7 +677,7 @@ function Gladius:UNIT_SPELLCAST_CHANNEL_START(event, unit)
if(not button) then return end
local castBar = self.buttons[unit].castBar
- if ( db.castBarOnCast ) then
+ if (db.castBar and db.castBarOnCast) then
castBar:Show()
end
castBar.isChanneling = true
@@ -705,7 +731,7 @@ function Gladius:UNIT_SPELLCAST_DELAYED(event, unit)
end
function Gladius:CastEnd(bar)
- if ( db.castBarOnCast ) then
+ if (db.castBar and db.castBarOnCast) then
bar:Hide()
end
bar.isCasting = nil
@@ -1703,15 +1729,6 @@ function Gladius:Test()
button.manaText:Hide()
end
- --set fake absorb value
- if( db.absorbBar ) then
- button.absorb.totalAbsorb:SetWidth(100-(i^2))
- button.absorb.totalAbsorbOverlay:SetWidth(100-(i^2))
- else
- button.absorb.totalAbsorb:SetWidth(0)
- button.absorb.totalAbsorbOverlay:SetWidth(0)
- end
-
--Check if it's a pet class and in that case update the frame to fit
button.isPetClass = petClasses[class] and true or false
@@ -1744,8 +1761,37 @@ function Gladius:Test()
end
+-- Utils used by absorbar
+function Gladius:SearchButtonByGUID(GUID)
+ for _, button in pairs(self.buttons) do
+ if( GUID and button.GUID == GUID) then
+ return button
+ end
+ end
+end
+
-- Absorb Update
-function Gladius:UpdateAbsorb(event, unit, button)
+function Gladius:EffectApplied(event, ...)
+ local sourceGUID, sourceName, destGUID, destName, spellId, value, quality, duration = ...
+ self:PreUpdateAbsorbBar(event, destGUID, destName)
+end
+function Gladius:EffectUpdated(event, ...)
+ local guid, spellId, value, duration = ...
+ self:PreUpdateAbsorbBar(event, guid)
+end
+function Gladius:EffectRemoved(event, ...)
+ local guid, spellId = ...
+ self:PreUpdateAbsorbBar(event, guid)
+end
+function Gladius:PreUpdateAbsorbBar(event, destGUID, name)
+ local button = self:SearchButtonByGUID(destGUID)
+ if (button) then
+ if( db.absorbBar and (arenaUnits[button.unit] == "playerUnit" or (arenaUnits[button.unit] ~= "playerUnit" and db.showPets)) ) then
+ self:UpdateAbsorbBar(event, button.unit, button)
+ end
+ end
+end
+function Gladius:UpdateAbsorbBar(event, unit, button)
local health = UnitHealth(unit)
local maxHealth = UnitHealthMax(unit)
@@ -1789,4 +1835,87 @@ function Gladius:UpdateAbsorb(event, unit, button)
button.absorb.overAbsorbGlow:Hide();
end
-end
\ No newline at end of file
+end
+
+-- HealComm (Heal Prediction)
+local function Update(self)
+ local unit = self.unit
+ local element = self.HealCommBar
+
+ --[[ Callback: HealthPrediction:PreUpdate(unit)
+ Called before the element has been updated.
+
+ * self - the HealthPrediction element
+ * unit - the unit for which the update has been triggered (string)
+ --]]
+ if element.PreUpdate then
+ element:PreUpdate(unit)
+ end
+
+ local guid = UnitGUID(unit)
+ local timeFrame = self.HealCommTimeframe and GetTime() + self.HealCommTimeframe or nil
+ local myIncomingHeal = HealComm:GetHealAmount(guid, HealComm.ALL_HEALS, timeFrame, UnitGUID("player")) or 0
+ local allIncomingHeal = HealComm:GetHealAmount(guid, HealComm.ALL_HEALS, timeFrame) or 0
+ local health = UnitHealth(unit)
+ local maxHealth = UnitHealthMax(unit)
+ local maxOverflowHP = maxHealth * element.maxOverflow
+ local otherIncomingHeal = 0
+
+ if health + allIncomingHeal > maxOverflowHP then
+ allIncomingHeal = maxOverflowHP - health
+ end
+
+ if allIncomingHeal < myIncomingHeal then
+ myIncomingHeal = allIncomingHeal
+ else
+ otherIncomingHeal = allIncomingHeal - myIncomingHeal
+ end
+
+ if element.myBar then
+ element.myBar:SetMinMaxValues(0, maxHealth)
+ element.myBar:SetValue(myIncomingHeal)
+ element.myBar:Show()
+ end
+
+ if element.otherBar then
+ element.otherBar:SetMinMaxValues(0, maxHealth)
+ element.otherBar:SetValue(otherIncomingHeal)
+ element.otherBar:Show()
+ end
+
+ --[[ Callback: HealthPrediction:PostUpdate(unit, myIncomingHeal, otherIncomingHeal)
+ Called after the element has been updated.
+
+ * self - the HealthPrediction element
+ * unit - the unit for which the update has been triggered (string)
+ * myIncomingHeal - the amount of incoming healing done by the player (number)
+ * otherIncomingHeal - the amount of incoming healing done by others (number)
+ --]]
+ if element.PostUpdate then
+ return element:PostUpdate(unit, myIncomingHeal, otherIncomingHeal)
+ end
+end
+local function MultiUpdate(...)
+ for i = 1, select("#", ...) do
+ --[[ for j = 1, #enabledUF do
+ local frame = enabledUF[j]
+
+ if frame.unit and frame:IsVisible() and UnitGUID(frame.unit) == select(i, ...) then
+ Path(frame)
+ end
+ end ]]
+ if (UnitGUID('arena1') == select(i, ...)) then
+ local allIncomingHeal = LHC:GetHealAmount(select(i, ...), LHC.ALL_HEALS, nil) or 0
+ print('updated arena1', allIncomingHeal)
+ elseif (UnitGUID('arena2') == select(i, ...)) then
+ local allIncomingHeal = LHC:GetHealAmount(select(i, ...), LHC.ALL_HEALS, nil) or 0
+ print('updated arena2', allIncomingHeal)
+ end
+ end
+end
+function Gladius:HealCommArena_Heal_Update(event, casterGUID, spellID, spellType, endTime, ...) -- ... = unpacked guid
+ MultiUpdate(...)
+end
+function Gladius:HealCommArena_Modified(event, guid)
+ MultiUpdate(guid)
+end
diff --git a/Gladius/Gladius.toc b/Gladius/Gladius.toc
index b2a1b56..45fe78c 100644
--- a/Gladius/Gladius.toc
+++ b/Gladius/Gladius.toc
@@ -4,7 +4,7 @@
## Version: v1.2.2
## Author: Proditor, Rinu, Enhanced by Cortes
## SavedVariables: GladiusDB
-## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0
+## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibHealCommArena-4.0
## LoadManagers: AddonLoader
## X-LoadOn-Slash: /gladius
## X-LoadOn-InterfaceOptions: Gladius
diff --git a/Gladius/cooldownlist.lua b/Gladius/cooldownlist.lua
index bc47844..68b5cae 100644
--- a/Gladius/cooldownlist.lua
+++ b/Gladius/cooldownlist.lua
@@ -14,6 +14,7 @@ function Gladius:GetCooldownList()
[12472] = { cd = 180, spec = L["Frost"], }, -- Icy Veins
[31687] = { cd = 180, spec = L["Frost"], }, -- Summon Water Elemental
[12043] = { cd = 120, spec = L["Arcane"], }, -- Presence of Mind
+ [42950] = { cd = 20, spec = L["Fire"] }, -- Combustion
[11129] = { cd = 120, spec = L["Fire"] }, -- Combustion
[12042] = { cd = 120, spec = L["Arcane"], }, -- Arcane Power
[11958] = { cd = 480, spec = L["Frost"], -- Coldsnap
@@ -48,6 +49,8 @@ function Gladius:GetCooldownList()
[49576] = 35, -- Death Grip
[47568] = 300, -- Empower Rune Weapon
[48743] = 120, -- Death Pact
+ [49039] = 120, -- Lichborne
+ [47481] = { cd = 60, spec = L["Unholy"], }, -- Pet Gnaw
[51052] = { cd = 120, spec = L["Unholy"], }, -- Anti-Magic Zone
[46584] = { cd = 180, notSpec = L["Unholy"], }, -- Raise Dead
[49206] = { cd = 180, spec = L["Unholy"], }, -- Summon Gargoyle
@@ -83,6 +86,7 @@ function Gladius:GetCooldownList()
[10278] = 300, -- Hand of Protection
[1044] = 25, -- Hand of Freedom
[54428] = 60, -- Divine Plea
+ [6940] = 120, -- Hand of Sacrifice
[64205] = 120, -- Divine Sacrifice
[10308] = { cd = 60, [L["Protection"]] = 40, }, -- Hammer of Justice
[642] = { cd = 300, -- Divine Shield
diff --git a/Gladius/embeds.xml b/Gladius/embeds.xml
index 73d45cb..8477ca5 100644
--- a/Gladius/embeds.xml
+++ b/Gladius/embeds.xml
@@ -1,18 +1,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Gladius/frame.lua b/Gladius/frame.lua
index 503ba7b..582e78b 100644
--- a/Gladius/frame.lua
+++ b/Gladius/frame.lua
@@ -4,8 +4,6 @@ local currentBracket
local LSM = LibStub("LibSharedMedia-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("Gladius", true)
-
-
local function DisableTexTiling(texture)
texture:SetHorizTile(false)
texture:SetVertTile(false)
@@ -640,7 +638,7 @@ function Gladius:CreatePetButton(id, parent)
absorbBar.overAbsorbGlow:SetBlendMode("ADD");
absorbBar.overAbsorbGlow:Hide()
-- Total absorb
- absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BORDER")
+ absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BACKGROUND")
absorbBar.totalAbsorb:Hide()
-- Total absorb overlay
absorbBar.totalAbsorbOverlay = absorbBar:CreateTexture(nil, "BORDER")
@@ -939,14 +937,28 @@ function Gladius:UpdateFrame()
button.absorb.totalAbsorb:ClearAllPoints()
button.absorb.totalAbsorb:SetTexture(LSM:Fetch(LSM.MediaType.STATUSBAR, db.barTexture)) -- same bar as health bar
button.absorb.totalAbsorb:SetPoint("LEFT", button.health:GetStatusBarTexture(), "RIGHT")
- button.absorb.totalAbsorb:SetSize((i+1)^2, button.health:GetHeight())
+ button.absorb.totalAbsorb:SetSize(0, button.health:GetHeight())
-- Total absorb overlay
button.absorb.totalAbsorbOverlay:ClearAllPoints()
button.absorb.totalAbsorbOverlay:SetHorizTile(true)
button.absorb.totalAbsorbOverlay:SetVertTile(true)
button.absorb.totalAbsorbOverlay:SetTexture([[Interface\AddOns\Gladius\media\RaidFrame\Shield-Overlay]], true, true)
button.absorb.totalAbsorbOverlay:SetPoint("LEFT", button.health:GetStatusBarTexture(), "RIGHT")
- button.absorb.totalAbsorbOverlay:SetSize((i+1)^2, button.health:GetHeight())
+ button.absorb.totalAbsorbOverlay:SetSize(0, button.health:GetHeight())
+
+ if( db.absorbBar and self.frame.testing ) then
+ button.absorb.totalAbsorb:Show()
+ button.absorb.totalAbsorbOverlay:Show()
+ button.absorb.overAbsorbGlow:Show()
+ button.absorb.totalAbsorb:SetWidth((i+1)^2)
+ button.absorb.totalAbsorbOverlay:SetWidth((i+1)^2)
+ else
+ button.absorb.overAbsorbGlow:Hide()
+ button.absorb.totalAbsorb:Hide()
+ button.absorb.totalAbsorbOverlay:Hide()
+ button.absorb.totalAbsorb:SetWidth(0)
+ button.absorb.totalAbsorbOverlay:SetWidth(0)
+ end
--mana bar location, size and texture
button.mana:ClearAllPoints()
@@ -1413,17 +1425,30 @@ function Gladius:UpdateFrame()
-- Update each cooldown icon
for i=1,14 do
local icon = button.spellCooldownFrame["icon"..i]
- icon:SetHeight(button.spellCooldownFrame:GetHeight()/2 - (db.cooldownIconMargin/2))
- icon:SetWidth(button.spellCooldownFrame:GetWidth()/2 - (db.cooldownIconMargin/2))
+ if db.cooldownOneLine then
+ icon:SetHeight(button.spellCooldownFrame:GetHeight() - db.cooldownIconMargin)
+ icon:SetWidth(button.spellCooldownFrame:GetWidth() - db.cooldownIconMargin)
+ else
+ icon:SetHeight(button.spellCooldownFrame:GetHeight()/2 - (db.cooldownIconMargin/2))
+ icon:SetWidth(button.spellCooldownFrame:GetWidth()/2 - (db.cooldownIconMargin/2))
+ end
icon:ClearAllPoints()
if(db.cooldownPos == "RIGHT") then
- if(i==1) then
- icon:SetPoint("TOPLEFT",button.spellCooldownFrame)
- elseif(i==2) then
- icon:SetPoint("TOP",button.spellCooldownFrame["icon"..i-1],"BOTTOM",0,-db.cooldownIconMargin)
- elseif(i>=3) then
- icon:SetPoint("LEFT",button.spellCooldownFrame["icon"..i-2],"RIGHT",db.cooldownIconMargin,0)
+ if db.cooldownOneLine then
+ if(i==1) then
+ icon:SetPoint("TOPLEFT",button.spellCooldownFrame,"TOPLEFT",0,-(db.cooldownIconMargin/2))
+ elseif(i>=2) then
+ icon:SetPoint("LEFT",button.spellCooldownFrame["icon"..i-1],"RIGHT",db.cooldownIconMargin,0)
+ end
+ else
+ if(i==1) then
+ icon:SetPoint("TOPLEFT",button.spellCooldownFrame)
+ elseif(i==2) then
+ icon:SetPoint("TOP",button.spellCooldownFrame["icon"..i-1],"BOTTOM",0,-db.cooldownIconMargin)
+ elseif(i>=3) then
+ icon:SetPoint("LEFT",button.spellCooldownFrame["icon"..i-2],"RIGHT",db.cooldownIconMargin,0)
+ end
end
else
if(i==1) then
@@ -1668,17 +1693,6 @@ function Gladius:UpdateFrame()
button.trinket:SetAlpha(alpha)
end
- --set fake absorb value
- if( db.absorbBar ) then
- button.absorb.overAbsorbGlow:Show()
- button.absorb.totalAbsorb:Show()
- button.absorb.totalAbsorbOverlay:Show()
- else
- button.absorb.overAbsorbGlow:Hide()
- button.absorb.totalAbsorb:Hide()
- button.absorb.totalAbsorbOverlay:Hide()
- end
-
end
Gladius:UpdateAttributes("arena"..i)
diff --git a/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua b/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua
index aa9ffd3..6130bfd 100644
--- a/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua
+++ b/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua
@@ -1,4 +1,3 @@
-
------------------------------------------------------------------------
--
-- AbsorbsMonitor
@@ -42,7 +41,7 @@ local AM_Core;
-- We have to upgrade from a previous version
if(upgraded) then
AM_Core = AM_Public.Core;
-
+
-- Since we have to keep the data structures intact, we will wipe all data.
-- Note that we wipe it instead of just replacing the tables with empty ones.
-- That way we don't leak the memory used by the previous version.
@@ -50,25 +49,25 @@ if(upgraded) then
if(AM_Public.Enabled) then
AM_Core.Disable();
end
-
+
-- This is the first loading of this library. We install everything that is stable across
-- multiple library versions: callbacks, event handling and other libraries
-- This code has to stay stable in its result across library versions!
else
AM_Public.Core = {Callbacks = LibStub:GetLibrary("CallbackHandler-1.0"):New(AM_Public), Events = {}};
AM_Core = AM_Public.Core;
-
+
local frame = CreateFrame("Frame", "AbsMon_Events");
local events = AM_Core.Events;
-
+
frame:SetScript("OnEvent",
function(self, event, ...)
events[event](...);
- end
+ end
);
-
+
AM_Core.Frame = frame;
-
+
LibStub("AceComm-3.0"):Embed(AM_Core);
LibStub("AceTimer-3.0"):Embed(AM_Core);
LibStub("AceSerializer-3.0"):Embed(AM_Core);
@@ -168,7 +167,7 @@ local OnScalingDecode = setmetatable({}, {
__index = function(table, class)
return table.DEFAULT;
end
-});
+});
-- Shortcut to the most important core functions
local ApplySingularEffect;
@@ -199,21 +198,21 @@ end
local function DeepTableCopy(src)
dest = {};
-
+
for k, v in pairs(src) do
if(type(k) == "table") then
k = DeepTableCopy(k);
end
-
+
if(type(v) == "table") then
v = DeepTableCopy(v);
end
-
+
dest[k] = v;
end
-
+
setmetatable(dest, getmetatable(src));
-
+
return dest;
end
@@ -230,28 +229,28 @@ local function GetUnitId(guid, name)
if(UnitGUID(name)) then
return name;
end
-
+
if(UnitGUID("target") == guid) then
return "target";
elseif(UnitGUID("focus") == guid) then
return "focus";
end
-
+
return nil;
end
-- Tries to get a working unitId and calls the given UnitXXX() function
local function UnitCall_bySearch(func, guid, name)
local result = func(name);
-
- if(not result) then
+
+ if(not result) then
if(UnitGUID("target") == guid) then
result = func("target");
elseif(UnitGUID("focus") == guid) then
result = func("focus");
end
- end
-
+ end
+
return result;
end
@@ -266,120 +265,120 @@ function AM_Core.Enable()
playerGUID = UnitGUID("player");
AM_Core.activeEffects = {bySpell = {}, byPriority = {}, Area = {}};
-
+
activeEffectsBySpell = AM_Core.activeEffects.bySpell;
activeEffectsByPriority = AM_Core.activeEffects.byPriority;
activeAreaEffects = AM_Core.activeEffects.Area;
-
+
AM_Core.activeCharges = {};
activeCharges = AM_Core.activeCharges;
-
+
Effects = AM_Core.Effects;
- AreaTriggers = AM_Core.AreaTriggers;
+ AreaTriggers = AM_Core.AreaTriggers;
CombatTriggersOnHeal = AM_Core.CombatTriggers.OnHeal;
CombatTriggersOnHealCrit = AM_Core.CombatTriggers.OnHealCrit;
CombatTriggersOnAuraApplied = AM_Core.CombatTriggers.OnAuraApplied;
CombatTriggersOnAuraRemoved = AM_Core.CombatTriggers.OnAuraRemoved;
-
+
AM_Core.UnitStats = {[playerGUID] = {playerClass, 0, 0, 1.0}};
UnitStats = AM_Core.UnitStats;
-
- AM_Core.Scaling = {[-1] = {}, [playerGUID] = {}}
+
+ AM_Core.Scaling = {[-1] = {}, [playerGUID] = {}}
Scaling = AM_Core.Scaling;
-
+
playerScaling = Scaling[playerGUID];
privateScaling = Scaling[-1];
-
+
AM_Core.RegisterEvent("ZONE_CHANGED_NEW_AREA");
AM_Events.ZONE_CHANGED_NEW_AREA();
if(playerClass == "DEATHKNIGHT") then
AM_Core.RegisterEvent("UNIT_ATTACK_POWER");
-
+
elseif(playerClass == "DRUID") then
AM_Core.RegisterEvent("UNIT_ATTACK_POWER");
-
+
elseif(playerClass == "MAGE") then
AM_Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS");
-
+
elseif(playerClass == "PALADIN") then
AM_Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS");
-
+
elseif(playerClass == "PRIEST") then
AM_Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS");
-
+
elseif(playerClass == "WARLOCK") then
AM_Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS");
end
-
+
AM_Events.STATS_CHANGED();
-
+
-- This has to happen before class init, else we get no initial scaling broadcast
if(not AM_Core.Silent) then
AM_Core.SetVerbose();
end
-
+
--AM_Core:EnableEffects();
-
+
if(OnEnableClass[playerClass]) then
OnEnableClass[playerClass]();
end
-
+
if(AM_Events.PLAYER_LEVEL_UP) then
AM_Core.RegisterEvent("PLAYER_LEVEL_UP");
end
-
+
if(AM_Events.PLAYER_TALENT_UPDATE) then
AM_Core.RegisterEvent("PLAYER_TALENT_UPDATE");
end
-
+
if(AM_Events.GLYPH_UPDATED) then
AM_Core.RegisterEvent("GLYPH_UPDATED");
end
-
+
if(AM_Events.PLAYER_EQUIPMENT_CHANGED) then
AM_Core.RegisterEvent("PLAYER_EQUIPMENT_CHANGED");
end
-
+
AM_Core:ScheduleRepeatingTimer(AM_Events.OnPeriodicBroadcast, 300);
-
+
AM_Core:RegisterComm("Absorbs_UnitStats", AM_Events.OnUnitStatsReceived);
- AM_Core:RegisterComm("Absorbs_Scaling", AM_Events.OnScalingReceived);
-
+ AM_Core:RegisterComm("Absorbs_Scaling", AM_Events.OnScalingReceived);
+
if(not AM_Public.Passive) then
AM_Core:SetActive();
end
-
+
AM_Public.Enabled = true;
end
-- These function has to clear any memory this version of the library may
-- have accumulated. It will be called in case this library version gets
-- replaced by a new one
-function AM_Core.Disable()
+function AM_Core.Disable()
for guid, effects in pairs(activeEffectsBySpell) do
AM_Callbacks:Fire("UnitCleared", guid);
end
-
+
wipe(AM_Core.activeEffects);
wipe(AM_Core.activeCharges);
-
+
wipe(AM_Core.Effects);
wipe(AM_Core.CombatTriggers);
wipe(AM_Core.AreaTriggers);
-
+
wipe(AM_Core.UnitStats);
wipe(AM_Core.Scaling);
-
+
wipe(AM_Core.Events);
AM_Core.Frame:UnregisterAllEvents();
-
+
AM_Core:UnregisterAllComm();
AM_Core:CancelAllTimers();
-
+
AM_Public.Enabled = false;
-
+
collectgarbage("collect");
end
@@ -399,64 +398,64 @@ function AM_Core.ApplySingularEffect(sourceGUID, sourceName, destGUID, destName,
local destEffects = activeEffectsBySpell[destGUID];
local effectInfo = Effects[spellId];
local effectEntry;
-
+
local value, quality, extra = effectInfo[3](sourceGUID, sourceName, destGUID, destName, spellId, destEffects);
-
+
if(value == nil) then return; end
-
+
-- No entry yet for this unit
if(not destEffects) then
effectEntry = {spellId, effectInfo[1], value, value, quality, 0, extra};
-
+
destEffects = {[-1] = 0, [-2] = 1.0, [spellId] = effectEntry};
-
+
activeEffectsBySpell[destGUID] = destEffects
activeEffectsByPriority[destGUID] = {effectEntry};
-
+
AM_Callbacks:Fire("EffectApplied", sourceGUID, sourceName, destGUID, destName, spellId, value, quality, effectInfo[2]);
-
+
-- Not this specific effect yet
- elseif(not destEffects[spellId]) then
+ elseif(not destEffects[spellId]) then
effectEntry = {spellId, effectInfo[1], value, value, quality, 0, extra};
-
+
destEffects[spellId] = effectEntry;
-
+
tinsert(activeEffectsByPriority[destGUID], effectEntry);
sort(activeEffectsByPriority[destGUID], SortEffects);
-
+
AM_Callbacks:Fire("EffectApplied", sourceGUID, sourceName, destGUID, destName, spellId, value, quality, effectInfo[2]);
-
+
-- Effect exists already
- else
+ else
effectEntry = destEffects[spellId];
local prevAmount = effectEntry[3];
-
+
effectEntry[3] = value;
effectEntry[4] = value;
effectEntry[5] = quality;
effectEntry[7] = extra;
-
+
sort(activeEffectsByPriority[destGUID], SortEffects);
-
+
AM_Callbacks:Fire("EffectUpdated", destGUID, spellId, value, quality, effectInfo[2]);
-
+
-- Adjust value in case this is a visible absorb to get the difference
value = value - prevAmount;
-
+
-- Cancel the exting duration timeout timer
AM_Core:CancelTimer(effectEntry[6], true);
end
-
+
if(quality < destEffects[-2]) then
destEffects[-2] = quality;
- end
-
+ end
+
if(effectInfo[1] < 2) then
destEffects[-1] = destEffects[-1] + value;
-
+
AM_Callbacks:Fire("UnitUpdated", destGUID, destEffects[-1], destEffects[-2]);
end
-
+
if(effectInfo[2]) then
-- We add a 5s grace period for latency and all kind of stuff
-- This duration timeout should only be needed if the unit moved out of combat log
@@ -465,55 +464,55 @@ function AM_Core.ApplySingularEffect(sourceGUID, sourceName, destGUID, destName,
else
effectEntry[6] = AM_Core:ScheduleRepeatingTimer(AM_Events.OnSingularActivityCheck, 8, {destGUID, spellId});
end
-
+
--AM_Core.Print("Applied buff effect "..spellId.." (priority: "..effectInfo[1]..") on "..destGUID.." for "..value..", new total: "..activeEffectsBySpell[destGUID][-1].." ("..#(activeEffectsByPriority[destGUID])..")");
end
-function AM_Core.ApplyAreaEffect(triggerGUID, triggerName, destGUID, destName, spellId)
+function AM_Core.ApplyAreaEffect(triggerGUID, triggerName, destGUID, destName, spellId)
if(not activeAreaEffects[triggerGUID]) then
return ApplySingularEffect(triggerGUID, triggerName, destGUID, destName, 0);
end
-
- local effectEntry = activeAreaEffects[triggerGUID];
+
+ local effectEntry = activeAreaEffects[triggerGUID];
local destEffects = activeEffectsBySpell[destGUID];
-
+
if(not destEffects) then
-- Quality of 1.1 to enforce message
destEffects = {[-1] = 0, [-2] = 1.1};
-
+
activeEffectsBySpell[destGUID] = destEffects;
activeEffectsByPriority[destGUID] = {};
-
+
-- At the moment it is impossible for such an effect to be refreshed
-- Since it is created by a summoned unit radiating it, it either
- -- gets removed/reapplied or removed/applied by a different unit.
- elseif(destEffects[spellId]) then
+ -- gets removed/reapplied or removed/applied by a different unit.
+ elseif(destEffects[spellId]) then
error("Called ApplyAreaEffect on refreshed aura");
return;
end
-
- destEffects[spellId] = effectEntry;
+
+ destEffects[spellId] = effectEntry;
effectEntry[9] = effectEntry[9] + 1; -- increase refcount
-
+
-- While we keep the trigger itself as a normal afflicted unit to keep
-- track when the area effect breaks, we do not broadcast it nor have to
-- keep a sorted priority list
if(triggerGUID ~= destGUID) then
tinsert(activeEffectsByPriority[destGUID], effectEntry);
- sort(activeEffectsByPriority[destGUID], SortEffects);
-
+ sort(activeEffectsByPriority[destGUID], SortEffects);
+
-- Note that we CANNOT use nil as an amount, since external addons can rely on this value being non-nil
-- for sorting. We're using -1 here that usually represents infinite values
AM_Callbacks:Fire("EffectApplied", triggerGUID, triggerName, destGUID, destName, spellId, -1, effectEntry[5], nil);
-
+
-- Update quality if needed
if(effectEntry[5] < destEffects[-2]) then
destEffects[-2] = effectEntry[5];
-
+
AM_Callbacks:Fire("UnitUpdated", destGUID, destEffects[-1], destEffects[-2]);
end
end
-
+
--AM_Core.Print("Applied area effect "..spellId.." (priority: "..effectData[1]..") on "..destGUID.." for "..value..", new total: "..activeEffectsBySpell[destGUID][-1].." ("..#(activeEffectsByPriority[destGUID])..")");
end
@@ -521,21 +520,21 @@ function AM_Core.CreateAreaTrigger(sourceGUID, sourceName, triggerGUID, triggerN
if(activeAreaEffects[triggerGUID]) then
error("Trying to create new area trigger on existing one, triggerGUID: "..triggerGUID..", existing spellId: "..activeAreaEffects[triggerGUID][1]);
return;
- end
+ end
local effectInfo = Effects[spellId];
-
+
local value, quality, extra = effectInfo[3](sourceGUID, sourceName, destGUID, destName, spellId, nil);
-
+
if(value == nil) then return; end
-
- --AM_Core.Print("Creating area tigger "..triggerGUID.."/"..triggerName.." from "..sourceName.." with spellId "..spellId.." for "..value.."/"..quality);
-
+
+ --AM_Core.Print("Creating area tigger "..triggerGUID.."/"..triggerName.." from "..sourceName.." with spellId "..spellId.." for "..value.."/"..quality);
+
local effectEntry = {spellId, -1 * effectInfo[1], value, value, quality, 0, extra, triggerGUID, 0};
effectEntry[6] = AM_Core:ScheduleTimer(AM_Events.OnAreaTimeout, effectInfo[2] + 5, effectEntry);
-
+
activeAreaEffects[triggerGUID] = effectEntry;
-
+
AM_Callbacks:Fire("AreaCreated", sourceGUID, sourceName, triggerGUID, spellId, value, quality);
end
@@ -544,30 +543,30 @@ function AM_Core.HitUnit(guid, absorbedTotal, overkill, spellSchool)
local guidEffects = activeEffectsBySpell[guid];
if(not guidEffects) then return; end
-
+
local absorbedRemaining = absorbedTotal;
-
+
local absorbed = 0;
local keepEffect, visibleAbsorb = false, false;
local i = 1;
local effectEntry;
-
+
-- This loop lasts as long as there is still an absorb value that
-- no effect could account for, but it will break once the list of
-- available effects got used completely.
while(absorbedRemaining > 0) do
effectEntry = activeEffectsByPriority[guid][i];
-
+
-- Sometimes there can be holes in this list since we don't re-sort
-- after removing an effect
if(effectEntry == nil) then break; end
-
+
-- Only absorb effects with exactly zero are ignored, negative ones
-- are treated as infinite (that is, no addon should display their value)
if(effectEntry[3] ~= 0) then
-- Hit the abosrb effect
absorbed, keepEffect = Effects[effectEntry[1]][4](effectEntry, absorbedRemaining, overkill, spellSchool);
-
+
if(absorbed > 0) then
-- Reduce the value of this effect and the remaining absorb value
-- to be accounted for
@@ -576,38 +575,38 @@ function AM_Core.HitUnit(guid, absorbedTotal, overkill, spellSchool)
if(effectEntry[8]) then
AM_Fire(AM_Callbacks, "AreaUpdated", effectEntry[8], effectEntry[3]);
-
- --AM_Core.Print("Hit area effect #"..i..", absorbed "..absorbed.." (keep: "..tostring(keepEffect).."), remaining: "..absorbedRemaining.." - unit: "..effectEntry[3].."/"..activeEffectsBySpell[guid][-1]);
- else
+
+ --AM_Core.Print("Hit area effect #"..i..", absorbed "..absorbed.." (keep: "..tostring(keepEffect).."), remaining: "..absorbedRemaining.." - unit: "..effectEntry[3].."/"..activeEffectsBySpell[guid][-1]);
+ else
-- If it should be visible (priority < 2), correct the total value
if(effectEntry[2] < 2) then
guidEffects[-1] = guidEffects[-1] - absorbed;
-
+
-- Shows us that at least one visible effect got hit
visibleAbsorb = true;
end
-
+
AM_Fire(AM_Callbacks, "EffectUpdated", guid, effectEntry[1], effectEntry[3]);
-
+
--AM_Core.Print("Hit effect #"..i..", absorbed "..absorbed.." (keep: "..tostring(keepEffect).."), remaining: "..absorbedRemaining.." - total: "..guidEffects[-1]);
- end
+ end
end
-
+
-- If the hit-function told us to remove the effect, do so
-- Note that only RemoveActiveEffect is allowed to remove it from the
-- list, we just set the value to zero, so it gets ignored on any hit.
-- This is do not come into any desync issues with the events, and to
-- keep the proper clean-up code in one place
- if(not keepEffect) then
+ if(not keepEffect) then
effectEntry[3] = 0;
end
end
-
+
i = i + 1;
- end
-
+ end
+
-- There are two possibilities when things are going wrong
- --
+ --
-- a) we guessed an absorb value too high, in that case it will
-- automatically be corrected when it breaks
--
@@ -627,10 +626,10 @@ function AM_Core.HitUnit(guid, absorbedTotal, overkill, spellSchool)
guidEffects[-2] = 0.0;
visibleAbsorb = true;
end
-
+
if(visibleAbsorb) then
AM_Fire(AM_Core, "UnitUpdated", guid, guidEffects[-1], guidEffects[-2]);
- end
+ end
end
-- Note that this method should NOT be called on non-existing effects or units
@@ -638,49 +637,49 @@ end
function AM_Core.RemoveActiveEffect(guid, spellId)
local guidEffects = activeEffectsBySpell[guid];
local effectEntry = guidEffects[spellId];
-
+
--AM_Core.Print("Removing absorb effect "..spellId.." on "..guid.." with "..guidEffects[spellId][3].." left, total: "..guidEffects[-1].." ("..#(activeEffectsByPriority[guid])..")");
-
+
-- This is a shared effect with a triggerGUID
if(effectEntry[8]) then
effectEntry[9] = effectEntry[9] - 1;
-
+
if(guid ~= effectEntry[8]) then
AM_Callbacks:Fire("EffectRemoved", guid, spellId);
end
-
+
if(effectEntry[9] == 0) then
if(effectEntry[6]) then
AM_Core:CancelTimer(effectEntry[6], true);
end
-
+
AM_Callbacks:Fire("AreaCleared", effectEntry[8]);
- end
-
+ end
+
else
if(effectEntry[2] < 2) then
- guidEffects[-1] = guidEffects[-1] - effectEntry[3];
-
+ guidEffects[-1] = guidEffects[-1] - effectEntry[3];
+
AM_Callbacks:Fire("UnitUpdated", guid, guidEffects[-1], guidEffects[-2]);
end
-
+
AM_Core:CancelTimer(effectEntry[6], true);
-
- AM_Callbacks:Fire("EffectRemoved", guid, spellId);
- end
-
- if(#(activeEffectsByPriority[guid]) == 1) then
+
+ AM_Callbacks:Fire("EffectRemoved", guid, spellId);
+ end
+
+ if(#(activeEffectsByPriority[guid]) == 1) then
activeEffectsBySpell[guid] = nil;
activeEffectsByPriority[guid] = nil;
-
+
AM_Callbacks:Fire("UnitCleared", guid);
else
guidEffects[spellId] = nil;
-
+
for k, v in pairs(activeEffectsByPriority[guid]) do
if(v[1] == spellId) then
tremove(activeEffectsByPriority[guid], k);
-
+
break;
end
end
@@ -696,10 +695,10 @@ function AM_Core.PushCharge(guid, spellId, amount, lifetime)
if(not guidCharges) then
activeCharges[guid] = { [spellId] = {lastCombatLogEvent + lifetime, amount} };
-
+
elseif(not guidCharges[spellId]) then
guidCharges[spellId] = {lastCombatLogEvent + lifetime, amount};
-
+
else
tinsert(guidCharges[spellId], lastCombatLogEvent + lifetime);
tinsert(guidCharges[spellId], amount);
@@ -708,32 +707,32 @@ end
function AM_Core.PopCharge(guid, spellId)
local guidCharges = activeCharges[guid];
-
+
if(guidCharges) then
local queue = activeCharges[guid][spellId];
-
+
-- For some weird reason, it will fail (true even on empty array)
-- if checked for queue[1] ?!?
if(queue and queue[2]) then
local chargeAmount;
- local chargeExpire;
-
+ local chargeExpire;
+
-- This loop will not be able to run infinitely
while(true) do
-- In this order we might save one table reshuffle
chargeAmount = tremove(queue, 2);
-
+
if(not chargeAmount) then return 0; end
-
+
chargeExpire = tremove(queue, 1);
-
+
if(chargeExpire > lastCombatLogEvent) then
return chargeAmount;
end
end
end
end
-
+
return 0;
end
@@ -743,11 +742,11 @@ function AM_Core.AddCombatTrigger(target, event, func)
if(not oldTrigger) then
eventTriggers[target] = func;
-
+
else
local listIndex = target.."_list";
local funcList = eventTriggers[listIndex];
-
+
-- There is already a list of callbacks, so we just add this one
if(funcList) then
for k, v in pairs(funcList) do
@@ -755,35 +754,35 @@ function AM_Core.AddCombatTrigger(target, event, func)
return;
end
end
-
+
tinsert(funcList, func);
-
+
-- We used a direct call so far, create a list and set up a handler
else
if(oldTrigger == func) then
return;
end
-
- funcList = {oldTrigger, func};
+
+ funcList = {oldTrigger, func};
eventTriggers[listIndex] = funcList;
-
+
local handler = function(...)
for k, v in pairs(funcList) do
v(...);
end
end
-
+
eventTriggers[target] = handler;
- end
+ end
end
end
function AM_Core.RemoveCombatTrigger(target, event, func)
local eventTriggers = AM_Core.CombatTriggers[event];
-
+
local listIndex = target.."_list";
local funcList = eventTriggers[listIndex];
-
+
-- We have a list of callbacks, reduce if possible
if(funcList) then
if(#funcList == 2) then
@@ -792,43 +791,43 @@ function AM_Core.RemoveCombatTrigger(target, event, func)
else
-- ATTENTION: We have to keep the table in place
-- because the handler references this table
-
+
local old_funcList = DeepTableCopy(funcList);
wipe(funcList);
-
+
for k, v in pairs(old_funcList) do
- if(v ~= func) then
+ if(v ~= func) then
tinsert(funcList, v);
end
end
end
-
+
-- It was a direct call anyway
else
eventTriggers[target] = nil;
- end
+ end
end
local lastAP, lastSP = 0, 0;
function AM_Core.SendUnitStats()
if(curChatChannel) then
local curAP, curSP = UnitStats[playerGUID][2], UnitStats[playerGUID][3];
-
+
if( (curAP ~= lastAP) or (curSP ~= lastSP) ) then
AM_Core:SendCommMessage("Absorbs_UnitStats", AM_Core:Serialize(playerGUID, playerClass, curAP, curSP), curChatChannel);
-
+
lastAP, lastSP = curAP, curSP;
-
+
CommStatsCooldown = true;
AM_Core:ScheduleTimer(ClearCommStatsCooldown, 15);
- end
+ end
end
end
function AM_Core.SendScaling()
- if(curChatChannel) then
+ if(curChatChannel) then
AM_Core:SendCommMessage("Absorbs_Scaling", AM_Core:Serialize(playerGUID, playerClass, AM_Events.OnScalingEncode()), curChatChannel);
-
+
CommScalingCooldown = true;
AM_Core:ScheduleTimer(ClearCommScalingCooldown, 30);
end
@@ -844,38 +843,38 @@ function AM_Core:ScheduleUniqueTimer(id, callback, delay, arg)
callback(arg);
activeTimers[id] = nil;
end, delay);
-
+
activeTimers[id] = 1;
end
end
function AM_Core.SetActive()
- AM_Core.RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
-
+ AM_Core.RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
+
AM_Public.Passive = false;
end
function AM_Core.SetPassive()
AM_Core.UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
-
+
AM_Public.Passive = true;
end
function AM_Core.SetVerbose()
AM_Core.RegisterEvent("PARTY_MEMBERS_CHANGED");
AM_Core.RegisterEvent("RAID_ROSTER_UPDATE");
-
+
AM_Public.Silent = false;
-
+
AM_Events.GROUPING_CHANGED();
end
function AM_Core.SetSilent()
AM_Core.UnregisterEvent("PARTY_MEMBERS_CHANGED");
AM_Core.UnregisterEvent("RAID_ROSTER_UPDATE");
-
+
AM_Public.Silent = true;
-
+
curChatChannel = nil;
end
@@ -896,18 +895,18 @@ function AM_Events.PLAYER_ENTERING_WORLD()
AM_Core.RegisterEvent("PLAYER_ALIVE");
else
AM_Core.Available = true;
-
+
AM_Core.Enable();
end
-
+
AM_Core.UnregisterEvent("PLAYER_ENTERING_WORLD");
end
function AM_Events.PLAYER_ALIVE()
AM_Core.Available = true;
-
+
AM_Core.Enable();
-
+
AM_Core.UnregisterEvent("PLAYER_ALIVE");
end
@@ -916,45 +915,45 @@ function AM_Events.COMBAT_LOG_EVENT_UNFILTERED(timestamp, type, sourceGUID, sour
if(type == "SWING_DAMAGE") then
local amount, overkill, school, resisted, blocked, absorbed = select(1, ...);
-
+
if(not absorbed) then return; end
-
+
HitUnit(destGUID, absorbed, amount, SCHOOL_MASK_PHYSICAL);
-
+
elseif(type == "RANGE_DAMAGE" or type == "SPELL_DAMAGE" or type == "SPELL_PERIODIC_DAMAGE") then
local spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed = select(1, ...);
-
+
if(not absorbed) then return; end
-
- HitUnit(destGUID, absorbed, amount, spellSchool);
-
+
+ HitUnit(destGUID, absorbed, amount, spellSchool);
+
elseif(type == "SWING_MISSED") then
local missType, amountMissed = select(1, ...);
-
- if(missType ~= "ABSORB") then return; end
-
+
+ if(missType ~= "ABSORB") then return; end
+
HitUnit(destGUID, amountMissed, 0, SCHOOL_MASK_PHYSICAL);
-
+
elseif(type == "RANGE_MISSED" or type == "SPELL_MISSED" or type == "SPELL_PERIODIC_MISSED") then
local spellId, spellName, spellSchool, missType, amountMissed = select(1, ...);
-
+
if(missType ~= "ABSORB") then return; end
-
+
HitUnit(destGUID, amountMissed, 0, spellSchool);
-
+
elseif(type == "SPELL_HEAL") then
- local spellId, spellName, spellSchool, amount, overhealing, absorb, critical = select(1, ...);
-
+ local spellId, spellName, spellSchool, amount, overhealing, absorb, critical = select(1, ...);
+
if(CombatTriggersOnHeal[sourceGUID]) then
CombatTriggersOnHeal[sourceGUID](sourceGUID, sourceName, destGUID, destName, spellId, amount, overhealing);
end
-
- if(critical and CombatTriggersOnHealCrit[sourceGUID]) then
+
+ if(critical and CombatTriggersOnHealCrit[sourceGUID]) then
CombatTriggersOnHealCrit[sourceGUID](sourceGUID, sourceName, destGUID, destName, spellId, amount, overhealing);
end
elseif(type == "SPELL_AURA_APPLIED") then
local spellId, spellName = select(1, ...);
-
+
if(Effects[spellId]) then
if(Effects[spellId][1] > 0) then
ApplySingularEffect(sourceGUID, sourceName, destGUID, destName, spellId);
@@ -962,14 +961,14 @@ function AM_Events.COMBAT_LOG_EVENT_UNFILTERED(timestamp, type, sourceGUID, sour
ApplyAreaEffect(sourceGUID, sourceName, destGUID, destName, spellId);
end
end
-
+
if(CombatTriggersOnAuraApplied[spellId]) then
CombatTriggersOnAuraApplied[spellId](sourceGUID, sourceName, destGUID, destName, spellId);
end
-
+
elseif(type == "SPELL_AURA_REFRESH") then
local spellId, spellName = select(1, ...);
-
+
if(Effects[spellId]) then
if(Effects[spellId][1] > 0) then
ApplySingularEffect(sourceGUID, sourceName, destGUID, destName, spellId);
@@ -977,38 +976,38 @@ function AM_Events.COMBAT_LOG_EVENT_UNFILTERED(timestamp, type, sourceGUID, sour
ApplyAreaEffect(sourceGUID, sourceName, destGUID, destName, spellId);
end
end
-
+
if(CombatTriggersOnAuraApplied[spellId]) then
CombatTriggersOnAuraApplied[spellId](sourceGUID, sourceName, destGUID, destName, spellId);
end
-
+
elseif(type == "SPELL_AURA_REMOVED") then
- local spellId = select(1, ...);
-
+ local spellId = select(1, ...);
+
if(Effects[spellId]) then
if(activeEffectsBySpell[destGUID] and activeEffectsBySpell[destGUID][spellId]) then
RemoveActiveEffect(destGUID, spellId);
end
end
-
+
if(CombatTriggersOnAuraRemoved[spellId]) then
CombatTriggersOnAuraRemoved[spellId](sourceGUID, sourceName, destGUID, destName, spellId);
end
-
+
elseif(type == "SPELL_SUMMON") then
local spellId = select(1, ...);
-
+
if(AreaTriggers[spellId]) then
CreateAreaTrigger(sourceGUID, sourceName, destGUID, destName, AreaTriggers[spellId]);
end
-
+
--CombatTriggers(spellId.."_OnSummon"](sourceGUID, sourceName, destGUID, destName, spellId);
end
end
function AM_Events.GROUPING_CHANGED()
-- Note that the order here is VERY important
- if(UnitInBattleground("player")) then
+ if(UnitInBattleground("player")) then
curChatChannel = "BATTLEGROUND";
elseif(UnitInRaid("player")) then
curChatChannel = "RAID";
@@ -1049,13 +1048,13 @@ end
function AM_Events.OnUnitStatsReceived(prefix, text, distribution, target)
if(not text) then return; end
-
+
local success, guid, class, ap, sp = AM_Core:Deserialize(text);
-
+
if(not(success and guid and class and ap and sp)) then return; end
-
+
if(guid == playerGUID) then return; end
-
+
if(not UnitStats[guid]) then
UnitStats[guid] = {class, ap, sp, 1.0};
else
@@ -1075,13 +1074,13 @@ end
function AM_Events.OnScalingReceived(prefix, text, distribution, target)
if(not text) then return; end
-
- local success, guid, class, inScaling = AM_Core:Deserialize(text);
-
+
+ local success, guid, class, inScaling = AM_Core:Deserialize(text);
+
if(not(success and guid and class and inScaling)) then return; end
-
+
if(guid == playerGUID) then return; end
-
+
(OnScalingDecode[class])(guid, inScaling);
end
@@ -1094,7 +1093,7 @@ function AM_Events.OnPeriodicBroadcast()
if(not CommStatsCooldown) then
AM_Core:ScheduleUniqueTimer("comm_stats", AM_Core.SendUnitStats, 5);
end
-
+
if(not CommScalingCooldown) then
AM_Core:ScheduleUniqueTimer("comm_scaling", AM_Core.SendScaling, 5);
end
@@ -1110,34 +1109,34 @@ end
function AM_Events.OnSingularActivityCheck(args)
local guid, spellId = args[1], args[2];
-
+
if(activeEffectsBySpell[guid] and activeEffectsBySpell[guid][spellId]) then
local _, _, _, _, _, name = GetPlayerInfoByGUID(guid);
-
+
-- We cannot track whether it's still on, remove it
if(not name) then
AM_Core:CancelTimer(activeEffectsBySpell[guid][spellId][6]);
-
+
RemoveActiveEffect(guid, spellId);
end
-
+
local stillActive = false;
-
+
for i=1, 40 do
local _, _, _, _, _, _, _, _, _, _, buffId = UnitBuff(name, i);
-
+
if(not buffId) then break; end
-
+
if(buffId == spellId) then
stillActive = true;
-
+
break;
end
end
-
+
if(not stillActive) then
AM_Core:CancelTimer(activeEffectsBySpell[guid][spellId][6]);
-
+
RemoveActiveEffect(guid, spellId);
end
else
@@ -1153,14 +1152,14 @@ function AM_Events.OnAreaTimeout(areaEntry)
for guid, guidEffects in pairs(activeEffectsBySpell) do
if(areaEntry[9] == 0) then return; end
-
+
for spellId, effectEntry in pairs(guidEffects) do
- if(effectEntry == areaEntry) then
- RemoveActiveEffect(guid, spellId);
+ if(effectEntry == areaEntry) then
+ RemoveActiveEffect(guid, spellId);
end
end
end
-
+
-- We're only here if we didn't reduce the refcount to zero
error("Positive refcount "..areaEntry[9].." remained for area effect "..areaEntry[1].." by trigger "..areaEntry[8]);
end
@@ -1210,13 +1209,13 @@ function AM_Public.PrintProfiling()
UpdateAddOnCPUUsage();
UpdateAddOnMemoryUsage();
-
+
AM_Core.Print("Mem: "..format("%.3f", GetAddOnMemoryUsage("AbsorbsMonitor")).." kB");
AM_Core.Print("Time: "..format("%.3f", GetAddOnCPUUsage("AbsorbsMonitor")).." ms");
AM_Core.Print("--- critical code paths (all times in ms) ---");
-
+
local funcTable;
-
+
funcTable = {
["ApplySingularEffect"] = ApplySingularEffect,
["HitUnit"] = HitUnit,
@@ -1224,17 +1223,17 @@ function AM_Public.PrintProfiling()
["OnCombatLogEvent"] = OnCombatLogEvent,
["SortEffects"] = SortEffects,
};
-
+
local v_type;
local time_self, time_combined, count;
-
+
for k, v in pairs(funcTable) do
v_type = type(v);
-
+
if(v_type == "function") then
time_self, count = GetFunctionCPUUsage(v, false);
time_combined = GetFunctionCPUUsage(v, true);
-
+
AM_Core.Print(k.." (#"..count.."): "..format("%.4f", time_self).." / "..format("%.4f", time_combined));
end
end
@@ -1242,30 +1241,30 @@ end
function AM_Public.Unit_Total(guid)
local guidEffects = activeEffectsBySpell[guid];
-
+
return (guidEffects and guidEffects[-1] or 0);
end
function AM_Public.Unit_Effect(guid, spellId)
local guidEffects = activeEffectsBySpell[guid];
-
+
if(guidEffects) then
if(guidEffects[spellId]) then
return guidEffects[spellId][3];
end
end
-
+
return 0;
end
function AM_Public.Unit_Stats(guid, missingQuality)
local guidStats = UnitStats[guid];
-
+
if(guidStats) then
return guidStats[2], guidStats[3], guidStats[4];
else
return 0, 0, missingQuality;
- end
+ end
end
function AM_Public.Unit_Scaling(guid, defaultScaling, defaultQuality)
@@ -1283,7 +1282,7 @@ end
function AM_Public.Unit_StatsAndScaling(guid, missingQuality, defaultScaling, defaultQuality)
local guidStats = UnitStats[guid];
local guidScaling = Scaling[guid];
-
+
if(guidStats) then
if(guidScaling) then
return guidStats[2], guidStats[3], guidStats[4], guidScaling, 1.0;
@@ -1313,7 +1312,7 @@ function AM_Public.ScheduleScalingBroadcast()
end
end
-function AM_Public.Test()
+function AM_Public.Test()
ApplySingularEffect("0x0", "source", "0x1a", "dest A", 27779);
ApplySingularEffect("0x0", "source", "0x1a", "dest A", 48066);
ApplySingularEffect("0x0", "source", "0x1b", "dest B", 31771);
@@ -1356,7 +1355,7 @@ end
-- coefficient.
-- Expects at effect[5] a table indexed by spellId with the base values and at
-- effect[6] the spellpower coefficient
-local function generic_SpellScalingByTable_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
+local function generic_SpellScalingByTable_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local effectInfo = Effects[spellId];
local _, sp, quality = Unit_Stats(sourceGUID, 0.1);
@@ -1397,24 +1396,24 @@ local deathknight_defaultScaling = {0};
function deathknight_AntiMagicShell_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local maxHealth = UnitCall_bySearch(UnitHealthMax, destGUID, destName);
-
+
if(maxHealth == 0) then
return 0, 0.0;
end
-
+
local sourceScaling, quality = Unit_Scaling(sourceGUID, deathknight_defaultScaling, 0.5);
-
+
return floor(maxHealth * 0.5), quality, (0.75 + sourceScaling[1]);
end
-local function deathknight_AntiMagicShell_Hit(effectEntry, absorbedRemaining, overkill, spellSchool)
+local function deathknight_AntiMagicShell_Hit(effectEntry, absorbedRemaining, overkill, spellSchool)
-- TODO: what happens to mixed school attacks?
- if(spellSchool == SCHOOL_MASK_PHYSICAL) then
+ if(spellSchool == SCHOOL_MASK_PHYSICAL) then
return 0, true;
end
-
+
local maxAbsorb = floor((absorbedRemaining + overkill) * effectEntry[7]);
-
+
if(effectEntry[3] < maxAbsorb) then
return effectEntry[3], false;
else
@@ -1430,12 +1429,12 @@ end
local function deathknight_AntiMagicZone_Hit(effectEntry, absorbedRemaining, overkill, spellSchool)
-- TODO: what happens to mixed school attacks?
- if(spellSchool == SCHOOL_MASK_PHYSICAL) then
+ if(spellSchool == SCHOOL_MASK_PHYSICAL) then
return 0, true;
end
-
+
local maxAbsorb = floor((absorbedRemaining + overkill) * 0.75);
-
+
if(effectEntry[3] < maxAbsorb) then
return effectEntry[3], false;
else
@@ -1446,15 +1445,15 @@ end
local function deathknight_OnTalentUpdate()
-- Magic Suppression
local _, _, _, _, t = GetTalentInfo(3, 18);
-
+
playerScaling[1] = deathknight_MS_Ranks[t];
-
+
AM_Public.ScheduleScalingBroadcast();
end
function OnEnableClass.DEATHKNIGHT()
AM_Events.PLAYER_TALENT_UPDATE = deathknight_OnTalentUpdate;
-
+
deathknight_OnTalentUpdate();
end
@@ -1470,9 +1469,9 @@ local function druid_SavageDefense_Create(sourceGUID, sourceName, destGUID, dest
return floor(ap * 0.25), quality;
end
-local function druid_SavageDefense_Hit(effectEntry, absorbedRemaining, overkill, spellSchool)
+local function druid_SavageDefense_Hit(effectEntry, absorbedRemaining, overkill, spellSchool)
-- TODO: what happens to mixed school attacks?
- if(spellSchool == SCHOOL_MASK_PHYSICAL) then
+ if(spellSchool == SCHOOL_MASK_PHYSICAL) then
return min(effectEntry[3], absorbedRemaining), false;
else
return 0, true;
@@ -1497,7 +1496,7 @@ local mage_Absorb_Spells = {
[10225] = 875,
[27218] = 1125,
[43010] = 1950,
-
+
-- Frost Ward
[6143] = 165,
[8461] = 290,
@@ -1506,7 +1505,7 @@ local mage_Absorb_Spells = {
[28609] = 875,
[32796] = 1125,
[43012] = 1950,
-
+
-- Ice Barrier
[11426] = 438,
[13031] = 549,
@@ -1516,7 +1515,7 @@ local mage_Absorb_Spells = {
[33405] = 1075,
[43038] = 2800,
[43039] = 3300,
-
+
-- Mana Shield
[1463] = 120,
[8494] = 210,
@@ -1536,7 +1535,7 @@ local mage_defaultScaling = {1.0};
-- No Downranking support here
local function mage_IceBarrier_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local _, sp, quality1, sourceScaling, quality2 = Unit_StatsAndScaling(sourceGUID, 0.3, mage_defaultScaling, 0.4);
-
+
return floor((mage_Absorb_Spells[spellId] + (sp * 0.8053)) * sourceScaling[1]), math.min(quality1, quality2);
end
@@ -1544,7 +1543,7 @@ local function mage_FireWard_Hit(effectEntry, absorbedRemaining, overkill, spell
if(spellSchool ~= SCHOOL_MASK_FIRE) then
return 0, true;
end
-
+
return generic_Hit(effectEntry, absorbedRemaining, overkill, spellSchool);
end
@@ -1552,32 +1551,32 @@ local function mage_FrostWard_Hit(effectEntry, absorbedRemaining, overkill, spel
if(spellSchool ~= SCHOOL_MASK_FROST) then
return 0, true;
end
-
+
return generic_Hit(effectEntry, absorbedRemaining, overkill, spellSchool);
end
local function mage_OnGlyphUpdated()
local glyphSpellId;
-
+
playerScaling[1] = 1.0;
for i = 1, 6 do
_, _, glyphSpellId = GetGlyphSocketInfo(i);
-
+
-- Glyph of Ice Barrier
if(glyphSpellId and glyphSpellId == 63095) then
playerScaling[1] = 1.3;
-
+
break;
end
end
-
- AM_Public.ScheduleScalingBroadcast();
+
+ AM_Public.ScheduleScalingBroadcast();
end
function OnEnableClass.MAGE()
AM_Events.GLYPH_UPDATED = mage_OnGlyphUpdated;
-
+
mage_OnGlyphUpdated();
end
@@ -1604,15 +1603,15 @@ local function paladin_OnTalentUpdate()
-- Divine Guardian
local _, _, _, _, t = GetTalentInfo(2, 9);
-
+
playerScaling[1] = 1 + (t * 0.1);
-
- AM_Public.ScheduleScalingBroadcast();
+
+ AM_Public.ScheduleScalingBroadcast();
end
function OnEnableClass.PALADIN()
AM_Events.PLAYER_TALENT_UPDATE = paladin_OnTalentUpdate;
-
+
paladin_OnTalentUpdate();
end
@@ -1647,7 +1646,7 @@ local priest_PWS_Ranks = {
-- Divine Aegis: [47753] = healFactor
local priest_defaultScaling = {[PRIEST_DIVINEAEGIS_SPELLID] = 0};
-do
+do
for k, v in pairs(priest_PWS_Ranks) do
priest_defaultScaling[v[1]] = {v[3], 0.809};
end
@@ -1659,9 +1658,9 @@ end
-- Computed: base, sp, DA
-local function priest_PowerWordShield_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
+local function priest_PowerWordShield_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local _, sp, quality1, sourceScaling, quality2 = Unit_StatsAndScaling(sourceGUID, 0.1, priest_defaultScaling, 0.1);
-
+
return floor((sourceScaling[spellId][1] + sp * sourceScaling[spellId][2]) * ZONE_MODIFIER), math.min(quality1, quality2);
end
@@ -1673,31 +1672,31 @@ local function priest_PowerWordBarrier_Hit(effectEntry, absorbedRemaining, overk
return 0, false;
end
-local function priest_DivineAegis_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
+local function priest_DivineAegis_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local existing = 0;
-
+
if(destEffects and destEffects[spellId]) then
existing = destEffects[spellId][3];
end
local charge = PopCharge(destGUID, spellId);
-
+
if(charge == 0) then
return existing, 0.0;
end
-
+
local destLevel = UnitCall_bySearch(UnitLevel, destGUID, destName);
- local quality = 1.0;
-
+ local quality = 1.0;
+
if(destLevel == 0) then
destLevel = 80;
quality = 0.4;
end
-
+
return min(destLevel * 125, existing + charge), quality;
end
--- I officially HATE Divine Aegis (and Val'anyr for that matter) from now.
+-- I officially HATE Divine Aegis (and Val'anyr for that matter) from now.
-- After extensive testing and parsing/filtering a few hours of combat log, I found the following facts:
-- * In MOST cases, every critical heal will trigger an AURA_APPLIED/AURA_REFRESHED event, even on multiple crits on
-- one penance and with both AURA events being triggered after all critical heals. In rare cases, there is only one...
@@ -1707,16 +1706,16 @@ end
-- is completely fucked up in those cases. If two priests channel penance at the same time, both crit, you can never say
-- who will get the AURA_APPLIED event credited (yes, I found both the first-hitting priest as well as the second-hitting
-- priest there!) and who the following AURA_REFRESHED events
-local function priest_DivineAegis_OnHealCrit(sourceGUID, sourceName, destGUID, destName, spellId, amount)
+local function priest_DivineAegis_OnHealCrit(sourceGUID, sourceName, destGUID, destName, spellId, amount)
-- We can do a direct access to Scaling here, since the callback is only in place if it was present
-- in the first place
-
+
PushCharge(destGUID, PRIEST_DIVINEAEGIS_SPELLID, floor(amount * Scaling[sourceGUID][PRIEST_DIVINEAEGIS_SPELLID]), 5.0);
end
local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
local guidScaling;
-
+
--AbsorbsMonitor:Print("Applying for "..guid.." ("..level..") with factors: "..baseFactor.." / "..spFactor.." / "..daFactor);
if(not Scaling[guid]) then
@@ -1725,7 +1724,7 @@ local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
else
guidScaling = Scaling[guid];
end
-
+
guidScaling[PRIEST_DIVINEAEGIS_SPELLID] = daFactor;
if(daFactor > 0) then
@@ -1733,9 +1732,9 @@ local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
else
AM_Core.RemoveCombatTrigger(guid, "OnHealCrit", priest_DivineAegis_OnHealCrit);
end
-
+
local rankValue, rankSP;
-
+
for k, v in pairs(priest_PWS_Ranks) do
if(v[2] <= level) then
if(level == 80) then
@@ -1744,7 +1743,7 @@ local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
-- TODO
rankValue = v[3];
end
-
+
-- Based on the assumption that the decrease in sp coefficient is linear,
-- only tested for level 80 though so far
-- Cataclysm will help us get rid of this crap again
@@ -1757,7 +1756,7 @@ local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
else
rankSP = spFactor;
end
-
+
guidScaling[v[1]] = {rankValue * baseFactor, rankSP};
end
end
@@ -1765,40 +1764,40 @@ end
local function priest_UpdatePlayerScaling()
privateScaling.base = ( (1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05)) );
-
+
local spFactor = 0.807;
spFactor = spFactor + (privateScaling["BorrowedTime"] * 0.08);
- spFactor = spFactor * (1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05));
- privateScaling.sp = spFactor;
-
+ spFactor = spFactor * (1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05));
+ privateScaling.sp = spFactor;
+
privateScaling.DA = (privateScaling["DivineAegis"] * 0.1) * (1 + (privateScaling["4pcRaid9"] * 0.03));
priest_ApplyScaling(playerGUID, UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA);
-
- AM_Public.ScheduleScalingBroadcast();
+
+ AM_Public.ScheduleScalingBroadcast();
end
local function priest_ScanTalents()
-- Twin Disciplines
local _, _, _, _, t = GetTalentInfo(1, 2);
privateScaling["TwinDisc"] = t;
-
+
-- Improved Power Word: Shield
local _, _, _, _, t = GetTalentInfo(1, 9);
privateScaling["ImpPWS"] = t;
-
+
-- Focused Power
local _, _, _, _, t = GetTalentInfo(1, 16);
privateScaling["FocusedPower"] = t;
-
+
-- Divine Aegis
- local _, _, _, _, t = GetTalentInfo(1, 24);
+ local _, _, _, _, t = GetTalentInfo(1, 24);
privateScaling["DivineAegis"] = t;
-
+
-- Borrowed Time
local _, _, _, _, t = GetTalentInfo(1, 27);
privateScaling["BorrowedTime"] = t;
-
+
-- Spiritual Healing
local _, _, _, _, t = GetTalentInfo(2, 16);
privateScaling["SpiritualHealing"] = t;
@@ -1806,28 +1805,28 @@ end
local function priest_ScanEquipment()
local n = 0;
-
+
-- Crimson Acolyte Raiment's 4-piece Bonus
if(IsEquippedItem(50765) or IsEquippedItem(51178) or IsEquippedItem(51261)) then n = 1; end
if(IsEquippedItem(50767) or IsEquippedItem(51175) or IsEquippedItem(51264)) then n = n + 1; end
if(IsEquippedItem(50768) or IsEquippedItem(51176) or IsEquippedItem(51263)) then n = n + 1; end
if(IsEquippedItem(50766) or IsEquippedItem(51179) or IsEquippedItem(51260)) then n = n + 1; end
if(IsEquippedItem(50769) or IsEquippedItem(51177) or IsEquippedItem(51262)) then n = n + 1; end
-
+
if(n >= 4) then
privateScaling["4pcRaid10"] = 1;
-
+
-- no way to have 4pcRaid9 now
- privateScaling["4pcRaid9"] = 0;
+ privateScaling["4pcRaid9"] = 0;
return;
else
privateScaling["4pcRaid10"] = 0;
end
-
+
n = 0;
-
+
-- Velen's/Zabra's Raiment 4-piece Bonus
-
+
-- UnitFactionGroup's first return value is NOT localized
if(UnitFactionGroup("player") == "Alliance") then
if(IsEquippedItem(47914) or IsEquippedItem(47984) or IsEquippedItem(48035)) then n = 1; end
@@ -1840,9 +1839,9 @@ local function priest_ScanEquipment()
if(IsEquippedItem(48071) or IsEquippedItem(48062) or IsEquippedItem(48061)) then n = n + 1; end
if(IsEquippedItem(48070) or IsEquippedItem(48063) or IsEquippedItem(48060)) then n = n + 1; end
if(IsEquippedItem(48067) or IsEquippedItem(48066) or IsEquippedItem(48057)) then n = n + 1; end
- if(IsEquippedItem(48069) or IsEquippedItem(48064) or IsEquippedItem(48059)) then n = n + 1; end
+ if(IsEquippedItem(48069) or IsEquippedItem(48064) or IsEquippedItem(48059)) then n = n + 1; end
end
-
+
if(n >= 4) then
privateScaling["4pcRaid9"] = 1;
else
@@ -1860,7 +1859,7 @@ local function priest_OnTalentUpdate()
end
local function priest_OnEquipmentChangedDelayed()
- priest_ScanEquipment();
+ priest_ScanEquipment();
priest_UpdatePlayerScaling();
end
@@ -1872,7 +1871,7 @@ local function priest_OnScalingEncode()
return {UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA};
end
-function OnScalingDecode.PRIEST(guid, in_guidScaling)
+function OnScalingDecode.PRIEST(guid, in_guidScaling)
if(#in_guidScaling ~= 4) then return; end
priest_ApplyScaling(guid, unpack(in_guidScaling));
@@ -1881,13 +1880,13 @@ end
function OnEnableClass.PRIEST()
AM_Events.PLAYER_LEVEL_UP = priest_OnLevelUp;
AM_Events.PLAYER_TALENT_UPDATE = priest_OnTalentUpdate;
- AM_Events.PLAYER_EQUIPMENT_CHANGED = priest_OnEquipmentChanged;
-
+ AM_Events.PLAYER_EQUIPMENT_CHANGED = priest_OnEquipmentChanged;
+
AM_Events.OnScalingEncode = priest_OnScalingEncode;
-
+
priest_ScanTalents();
priest_ScanEquipment();
-
+
priest_UpdatePlayerScaling();
end
@@ -1904,7 +1903,7 @@ local shaman_defaultScaling = {0.3};
local function shaman_AstralShift_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local sourceScaling, quality = Unit_Scaling(sourceGUID, shaman_defaultScaling, 0.7);
-
+
return -1, quality, sourceScaling[1];
end
@@ -1917,15 +1916,15 @@ end
local function shaman_OnTalentUpdate()
-- Astral Shift
local _, _, _, _, t = GetTalentInfo(1, 21);
-
+
playerScaling[1] = t * 0.1;
-
+
AM_Public.ScheduleScalingBroadcast();
end
function OnEnableClass.SHAMAN()
AM_Events.PLAYER_TALENT_UPDATE = shaman_OnTalentUpdate;
-
+
shaman_OnTalentUpdate();
end
@@ -1975,22 +1974,22 @@ local function warlock_ShadowWard_Hit(effectEntry, absorbedRemaining, overkill,
if(spellSchool ~= SCHOOL_MASK_SHADOW) then
return 0, true;
end
-
+
return generic_Hit(effectEntry, absorbedRemaining, overkill, spellSchool);
end
local function warlock_OnTalentUpdate()
-- Demonic Brutality
local _, _, _, _, t = GetTalentInfo(2, 6);
-
+
playerScaling[1] = 1 + (t * 0.1);
-
- AM_Public.ScheduleScalingBroadcast();
+
+ AM_Public.ScheduleScalingBroadcast();
end
function OnEnableClass.WARLOCK()
AM_Events.PLAYER_TALENT_UPDATE = warlock_OnTalentUpdate;
-
+
warlock_OnTalentUpdate();
end
@@ -2026,17 +2025,17 @@ end
local function items_Valanyr_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local existing = 0;
-
+
if(destEffects and destEffects[spellId]) then
existing = destEffects[spellId][3];
end
local charge = PopCharge(destGUID, spellId);
-
+
if(charge == 0) then
return existing, 0.0;
end
-
+
-- According to the blue post explaining the Val'anyr effect on introduction, all units are
-- contributing to the same bubble with a cap of 20.000
return min(20000, existing + charge), 1.0;
@@ -2048,12 +2047,12 @@ end
local function items_Stoicism_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects)
local maxHealth = UnitCall_bySearch(UnitHealthMax, destGUID, destName);
-
+
if(maxHealth == 0) then
return 0, 0.0;
end
-
- return floor(maxHealth * 0.2), 1.0;
+
+ return floor(maxHealth * 0.2), 1.0;
end
@@ -2067,7 +2066,7 @@ local mage_FrostWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, mage_
local mage_IceBarrier_Entry = {1.0, 60, mage_IceBarrier_Create, generic_Hit};
local mage_ManaShield_Entry = {1.0, 60, generic_SpellScalingByTable_Create, generic_Hit, mage_Absorb_Spells, 0.8053};
-local priest_PWS_Entry = {1.0, 30, priest_PowerWordShield_Create, generic_Hit};
+local priest_PWS_Entry = {1.0, 30, priest_PowerWordShield_Create, generic_Hit};
local warlock_Sacrifice_Entry = {1.0, 30, generic_ConstantByTable_Create, generic_Hit, warlock_Sacrifice_Spells};
local warlock_ShadowWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, warlock_ShadowWard_Hit, warlock_ShadowWard_Spells, 0.8053};
@@ -2080,17 +2079,17 @@ AM_Core.Effects = {
-- This is used when a known effect is applied, but it is impossible to properly account for it,
-- for example if an AREA effect is applied with an unknown trigger
[0] = {1.0, 0, function() return 0, 0.0; end, nil};
-
- -- DEATH KNIGHT
+
+ -- DEATH KNIGHT
-- Anti-Magic Shell
[48707] = {3.0, 5, deathknight_AntiMagicShell_Create, deathknight_AntiMagicShell_Hit},
-- Anti-Magic Zone
[50461] = {-3.0, 10, deathknight_AntiMagicZone_Create, deathknight_AntiMagicZone_Hit},
-
+
-- DRUID
-- Savage Defense
[62606] = {1.1, 10, druid_SavageDefense_Create, druid_SavageDefense_Hit},
-
+
-- MAGE
-- Fire Ward
[543] = mage_FireWard_Entry,
@@ -2116,7 +2115,7 @@ AM_Core.Effects = {
[27134] = mage_IceBarrier_Entry,
[33405] = mage_IceBarrier_Entry,
[43038] = mage_IceBarrier_Entry,
- [43039] = mage_IceBarrier_Entry,
+ [43039] = mage_IceBarrier_Entry,
-- Mana Shield
[1463] = mage_ManaShield_Entry,
[8494] = mage_ManaShield_Entry,
@@ -2126,14 +2125,14 @@ AM_Core.Effects = {
[10193] = mage_ManaShield_Entry,
[27131] = mage_ManaShield_Entry,
[43019] = mage_ManaShield_Entry,
- [43020] = mage_ManaShield_Entry,
-
+ [43020] = mage_ManaShield_Entry,
+
-- PALADIN
-- Sacred Shield
[58597] = {1.0, 6, paladin_SacredShield_Create, generic_Hit},
-- Ardent Defender
-
- -- PRIEST
+
+ -- PRIEST
-- Power Word: Shield
[17] = priest_PWS_Entry,
[592] = priest_PWS_Entry,
@@ -2152,13 +2151,13 @@ AM_Core.Effects = {
[47753] = {1.0, 12, priest_DivineAegis_Create, generic_Hit},
-- Power Word: Barrier
[81781] = {-1.0, 25, priest_PowerWordBarrier_Create, priest_PowerWordBarrier_Hit},
-
+
-- ROGUE
-- Cheat Death
-- No clue how it works technically (does it still uses an absorb?)
-- While there is no absorb value to watch for, it would screw our
-- numbers on other effects.
-
+
-- SHAMAN
-- Astral Shift
[52179] = {2.5, nil, shaman_AstralShift_Create, shaman_AstralShift_Hit},
@@ -2166,7 +2165,7 @@ AM_Core.Effects = {
-- This absorb effect cannot be supported without heavy exceptional
-- handling, because it is neither applied by SPELL_AURA_APPLIED or
-- nor removed by SPELL_AURA_REMOVED - but only an UNIT_AURA event is
- -- fired on gain/loss. The gain can actually be detected by the cast
+ -- fired on gain/loss. The gain can actually be detected by the cast
-- of Stoneclaw Totem (and knowledge about the glyphs), but not if it
-- breaks prematurely, making looping through all buffs needed everytime
-- it could be active and UNIT_AURA is fired. At least the amount would
@@ -2175,7 +2174,7 @@ AM_Core.Effects = {
-- there is no proper removal event, so it would rely on zombified absorb
-- effects until the totem drops. Maybe later. (What about totem GUIDs?)
-- [55277] = {1.0, shaman_GlyphStoneclawTotem_Create, generic_Hit},
-
+
-- WARLOCK
-- Sacrifice
[7812] = warlock_Sacrifice_Entry,
@@ -2195,7 +2194,7 @@ AM_Core.Effects = {
[47890] = warlock_ShadowWard_Entry,
[47891] = warlock_ShadowWard_Entry,
- -- ITEMS
+ -- ITEMS
-- Val'anyr (spellId of the created absorb effect)
[64413] = {1.0, 8, items_Valanyr_Create, generic_Hit},
-- Essence of Gossamer
@@ -2232,8 +2231,8 @@ AM_Core.Effects = {
[29719] = {1.0, nil, function() return 4000, 1.0; end, generic_Hit},
-- Stoicism (Warrior Raid Set 10 4pc bonus)
[70845] = {1.0, 10, items_Stoicism_Create, generic_Hit},
-
-
+
+
-- Scarab Brooch
-- This trinket is a bit more problematic, because it creates non-stacking
-- shields, having always the strongest up. The question is now, what happens
@@ -2246,24 +2245,24 @@ AM_Core.Effects = {
AM_Core.AreaTriggers = {
-- Anti-Magic Zone
[51052] = 50461,
-
+
-- Power Word: Barrier
[62618] = 81781,
};
-AM_Core.CombatTriggers = {
+AM_Core.CombatTriggers = {
OnAuraApplied = {
[64411] = items_Valanyr_OnAuraApplied,
},
-
+
OnAuraRemoved = {
[64411] = items_Valanyr_OnAuraRemoved,
},
-
+
OnHeal = {
},
-
+
OnHealCrit = {
}
};
diff --git a/Gladius/libs/LibHealCommArena-4.0/Changelog-LibHealComm-4.0-v1.6.6.txt b/Gladius/libs/LibHealCommArena-4.0/Changelog-LibHealComm-4.0-v1.6.6.txt
new file mode 100644
index 0000000..c7ecfeb
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/Changelog-LibHealComm-4.0-v1.6.6.txt
@@ -0,0 +1,13 @@
+tag v1.6.6
+e5976a3af453c2809418ef39c3a78f7b3896f951
+Shadowed
+2010-06-28 02:34:28 -0700
+
+Tagging as release 1.6.6
+
+
+--------------------
+
+Shadowed:
+ - Incoming heals are now ignored if the healer is currently mind controlled.
+ - Added hardening to stop a rare-ish nil guid passing through to heals.
diff --git a/Gladius/libs/LibHealCommArena-4.0/ChatThrottleLib.lua b/Gladius/libs/LibHealCommArena-4.0/ChatThrottleLib.lua
new file mode 100644
index 0000000..b0afc92
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/ChatThrottleLib.lua
@@ -0,0 +1,503 @@
+--
+-- ChatThrottleLib by Mikk
+--
+-- Manages AddOn chat output to keep player from getting kicked off.
+--
+-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
+-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
+--
+-- Priorities get an equal share of available bandwidth when fully loaded.
+-- Communication channels are separated on extension+chattype+destination and
+-- get round-robinned. (Destination only matters for whispers and channels,
+-- obviously)
+--
+-- Will install hooks for SendChatMessage and SendAddonMessage to measure
+-- bandwidth bypassing the library and use less bandwidth itself.
+--
+--
+-- Fully embeddable library. Just copy this file into your addon directory,
+-- add it to the .toc, and it's done.
+--
+-- Can run as a standalone addon also, but, really, just embed it! :-)
+--
+
+local CTL_VERSION = 21
+
+local _G = _G
+
+if _G.ChatThrottleLib then
+ if _G.ChatThrottleLib.version >= CTL_VERSION then
+ -- There's already a newer (or same) version loaded. Buh-bye.
+ return
+ elseif not _G.ChatThrottleLib.securelyHooked then
+ print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, =v16) in it!")
+ -- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
+ -- ... and if someone has securehooked, they can kiss that goodbye too... >.<
+ _G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
+ if _G.ChatThrottleLib.ORIG_SendAddonMessage then
+ _G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
+ end
+ end
+ _G.ChatThrottleLib.ORIG_SendChatMessage = nil
+ _G.ChatThrottleLib.ORIG_SendAddonMessage = nil
+end
+
+if not _G.ChatThrottleLib then
+ _G.ChatThrottleLib = {}
+end
+
+ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
+local ChatThrottleLib = _G.ChatThrottleLib
+
+ChatThrottleLib.version = CTL_VERSION
+
+
+
+------------------ TWEAKABLES -----------------
+
+ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
+ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
+
+ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
+
+ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
+
+
+local setmetatable = setmetatable
+local table_remove = table.remove
+local tostring = tostring
+local GetTime = GetTime
+local math_min = math.min
+local math_max = math.max
+local next = next
+local strlen = string.len
+local GetFrameRate = GetFrameRate
+
+
+
+-----------------------------------------------------------------------
+-- Double-linked ring implementation
+
+local Ring = {}
+local RingMeta = { __index = Ring }
+
+function Ring:New()
+ local ret = {}
+ setmetatable(ret, RingMeta)
+ return ret
+end
+
+function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
+ if self.pos then
+ obj.prev = self.pos.prev
+ obj.prev.next = obj
+ obj.next = self.pos
+ obj.next.prev = obj
+ else
+ obj.next = obj
+ obj.prev = obj
+ self.pos = obj
+ end
+end
+
+function Ring:Remove(obj)
+ obj.next.prev = obj.prev
+ obj.prev.next = obj.next
+ if self.pos == obj then
+ self.pos = obj.next
+ if self.pos == obj then
+ self.pos = nil
+ end
+ end
+end
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for pipes
+-- A pipe is a plain integer-indexed queue, which also happens to be a ring member
+
+ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
+local PipeBin = setmetatable({}, {__mode="k"})
+
+local function DelPipe(pipe)
+ for i = #pipe, 1, -1 do
+ pipe[i] = nil
+ end
+ pipe.prev = nil
+ pipe.next = nil
+
+ PipeBin[pipe] = true
+end
+
+local function NewPipe()
+ local pipe = next(PipeBin)
+ if pipe then
+ PipeBin[pipe] = nil
+ return pipe
+ end
+ return {}
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for messages
+
+ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
+local MsgBin = setmetatable({}, {__mode="k"})
+
+local function DelMsg(msg)
+ msg[1] = nil
+ -- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
+ MsgBin[msg] = true
+end
+
+local function NewMsg()
+ local msg = next(MsgBin)
+ if msg then
+ MsgBin[msg] = nil
+ return msg
+ end
+ return {}
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:Init
+-- Initialize queues, set up frame for OnUpdate, etc
+
+
+function ChatThrottleLib:Init()
+
+ -- Set up queues
+ if not self.Prio then
+ self.Prio = {}
+ self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ end
+
+ -- v4: total send counters per priority
+ for _, Prio in pairs(self.Prio) do
+ Prio.nTotalSent = Prio.nTotalSent or 0
+ end
+
+ if not self.avail then
+ self.avail = 0 -- v5
+ end
+ if not self.nTotalSent then
+ self.nTotalSent = 0 -- v5
+ end
+
+
+ -- Set up a frame to get OnUpdate events
+ if not self.Frame then
+ self.Frame = CreateFrame("Frame")
+ self.Frame:Hide()
+ end
+ self.Frame:SetScript("OnUpdate", self.OnUpdate)
+ self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
+ self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ self.OnUpdateDelay = 0
+ self.LastAvailUpdate = GetTime()
+ self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
+
+ -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
+ if not self.securelyHooked then
+ -- Use secure hooks as of v16. Old regular hook support yanked out in v21.
+ self.securelyHooked = true
+ --SendChatMessage
+ hooksecurefunc("SendChatMessage", function(...)
+ return ChatThrottleLib.Hook_SendChatMessage(...)
+ end)
+ --SendAddonMessage
+ hooksecurefunc("SendAddonMessage", function(...)
+ return ChatThrottleLib.Hook_SendAddonMessage(...)
+ end)
+ end
+ self.nBypass = 0
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
+
+local bMyTraffic = false
+
+function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = tostring(text or ""):len() + tostring(prefix or ""):len();
+ size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:UpdateAvail
+-- Update self.avail with how much bandwidth is currently available
+
+function ChatThrottleLib:UpdateAvail()
+ local now = GetTime()
+ local MAX_CPS = self.MAX_CPS;
+ local newavail = MAX_CPS * (now - self.LastAvailUpdate)
+ local avail = self.avail
+
+ if now - self.HardThrottlingBeginTime < 5 then
+ -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
+ avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
+ self.bChoking = true
+ elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs
+ avail = math_min(MAX_CPS, avail + newavail*0.5)
+ self.bChoking = true -- just a statistic
+ else
+ avail = math_min(self.BURST, avail + newavail)
+ self.bChoking = false
+ end
+
+ avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
+
+ self.avail = avail
+ self.LastAvailUpdate = now
+
+ return avail
+end
+
+
+-----------------------------------------------------------------------
+-- Despooling logic
+
+function ChatThrottleLib:Despool(Prio)
+ local ring = Prio.Ring
+ while ring.pos and Prio.avail > ring.pos[1].nSize do
+ local msg = table_remove(Prio.Ring.pos, 1)
+ if not Prio.Ring.pos[1] then
+ local pipe = Prio.Ring.pos
+ Prio.Ring:Remove(pipe)
+ Prio.ByName[pipe.name] = nil
+ DelPipe(pipe)
+ else
+ Prio.Ring.pos = Prio.Ring.pos.next
+ end
+ Prio.avail = Prio.avail - msg.nSize
+ bMyTraffic = true
+ msg.f(unpack(msg, 1, msg.n))
+ bMyTraffic = false
+ Prio.nTotalSent = Prio.nTotalSent + msg.nSize
+ DelMsg(msg)
+ if msg.callbackFn then
+ msg.callbackFn (msg.callbackArg)
+ end
+ end
+end
+
+
+function ChatThrottleLib.OnEvent(this,event)
+ -- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
+ local self = ChatThrottleLib
+ if event == "PLAYER_ENTERING_WORLD" then
+ self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
+ self.avail = 0
+ end
+end
+
+
+function ChatThrottleLib.OnUpdate(this,delay)
+ local self = ChatThrottleLib
+
+ self.OnUpdateDelay = self.OnUpdateDelay + delay
+ if self.OnUpdateDelay < 0.08 then
+ return
+ end
+ self.OnUpdateDelay = 0
+
+ self:UpdateAvail()
+
+ if self.avail < 0 then
+ return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
+ end
+
+ -- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
+ local n = 0
+ for prioname,Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ n = n + 1
+ end
+ end
+
+ -- Anything queued still?
+ if n<1 then
+ -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
+ for prioname, Prio in pairs(self.Prio) do
+ self.avail = self.avail + Prio.avail
+ Prio.avail = 0
+ end
+ self.bQueueing = false
+ self.Frame:Hide()
+ return
+ end
+
+ -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
+ local avail = self.avail/n
+ self.avail = 0
+
+ for prioname, Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ Prio.avail = Prio.avail + avail
+ if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
+ self:Despool(Prio)
+ -- Note: We might not get here if the user-supplied callback function errors out! Take care!
+ end
+ end
+ end
+
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Spooling logic
+
+
+function ChatThrottleLib:Enqueue(prioname, pipename, msg)
+ local Prio = self.Prio[prioname]
+ local pipe = Prio.ByName[pipename]
+ if not pipe then
+ self.Frame:Show()
+ pipe = NewPipe()
+ pipe.name = pipename
+ Prio.ByName[pipename] = pipe
+ Prio.Ring:Add(pipe)
+ end
+
+ pipe[#pipe + 1] = msg
+
+ self.bQueueing = true
+end
+
+
+
+function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = text:len()
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendChatMessage(text, chattype, language, destination)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg)
+ end
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendChatMessage
+ msg[1] = text
+ msg[2] = chattype or "SAY"
+ msg[3] = language
+ msg[4] = destination
+ msg.n = 4
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
+end
+
+
+function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = prefix:len() + 1 + text:len();
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD;
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendAddonMessage(prefix, text, chattype, target)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg)
+ end
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendAddonMessage
+ msg[1] = prefix
+ msg[2] = text
+ msg[3] = chattype
+ msg[4] = target
+ msg.n = (target~=nil) and 4 or 3;
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Get the ball rolling!
+
+ChatThrottleLib:Init()
+
+--[[ WoWBench debugging snippet
+if(WOWB_VER) then
+ local function SayTimer()
+ print("SAY: "..GetTime().." "..arg1)
+ end
+ ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
+ ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
+end
+]]
+
+
diff --git a/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.lua b/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.lua
new file mode 100644
index 0000000..e9d508c
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.lua
@@ -0,0 +1,2861 @@
+local major = "LibHealCommArena-4.0"
+local minor = 67
+assert(LibStub, string.format("%s requires LibStub.", major))
+
+local HealComm = LibStub:NewLibrary(major, minor)
+if( not HealComm ) then return end
+
+local pairs = pairs
+local rawget, rawset = rawget, rawset
+local select = select
+local setmetatable = setmetatable
+local tonumber = tonumber
+local type = type
+local unpack = unpack
+local band, bor = bit.band, bit.bor
+local ceil, floor, max, min = math.ceil, math.floor, math.max, math.min
+local byte, char, format, gsub, len, match, split = string.byte, string.char, string.format, string.gsub, string.len, string.match, string.split
+local tconcat, tinsert, tremove, twipe = table.concat, table.insert, table.remove, table.wipe
+
+local GetGlyphSocketInfo = GetGlyphSocketInfo
+local GetInventoryItemLink = GetInventoryItemLink
+local GetNumGlyphSockets = GetNumGlyphSockets
+local GetNumPartyMembers = GetNumPartyMembers
+local GetNumRaidMembers = GetNumRaidMembers
+local GetNumSpellTabs = GetNumSpellTabs
+local GetNumTalentTabs = GetNumTalentTabs
+local GetNumTalents = GetNumTalents
+local GetRaidRosterInfo = GetRaidRosterInfo
+local GetSpellBonusHealing = GetSpellBonusHealing
+local GetSpellCritChance = GetSpellCritChance
+local GetSpellInfo = GetSpellInfo
+local GetSpellName = GetSpellName
+local GetSpellTabInfo = GetSpellTabInfo
+local GetTalentInfo = GetTalentInfo
+local GetTime = GetTime
+local GetZonePVPInfo = GetZonePVPInfo
+local InCombatLockdown = InCombatLockdown
+local IsEquippedItem = IsEquippedItem
+local IsInInstance = IsInInstance
+local IsSpellInRange = IsSpellInRange
+local SpellIsTargeting = SpellIsTargeting
+local UnitAura = UnitAura
+local UnitBuff = UnitBuff
+local UnitCanAssist = UnitCanAssist
+local UnitCastingInfo = UnitCastingInfo
+local UnitChannelInfo = UnitChannelInfo
+local UnitExists = UnitExists
+local UnitGUID = UnitGUID
+local UnitHasVehicleUI = UnitHasVehicleUI
+local UnitIsCharmed = UnitIsCharmed
+local UnitIsVisible = UnitIsVisible
+local UnitLevel = UnitLevel
+local UnitName = UnitName
+local UnitPlayerControlled = UnitPlayerControlled
+
+local BOOKTYPE_SPELL = BOOKTYPE_SPELL
+local MAX_PARTY_MEMBERS = MAX_PARTY_MEMBERS
+local MAX_PLAYER_LEVEL = MAX_PLAYER_LEVEL
+local MAX_RAID_MEMBERS = MAX_RAID_MEMBERS
+
+-- API CONSTANTS
+local ALL_DATA = 0x0f
+local DIRECT_HEALS = 0x01
+local CHANNEL_HEALS = 0x02
+local HOT_HEALS = 0x04
+local ABSORB_SHIELDS = 0x08
+local BOMB_HEALS = 0x10
+local ALL_HEALS = bor(DIRECT_HEALS, CHANNEL_HEALS, HOT_HEALS, BOMB_HEALS)
+local CASTED_HEALS = bor(DIRECT_HEALS, CHANNEL_HEALS)
+local OVERTIME_HEALS = bor(HOT_HEALS, CHANNEL_HEALS)
+
+HealComm.ALL_HEALS, HealComm.CHANNEL_HEALS, HealComm.DIRECT_HEALS, HealComm.HOT_HEALS, HealComm.CASTED_HEALS, HealComm.ABSORB_SHIELDS, HealComm.ALL_DATA, HealComm.BOMB_HEALS = ALL_HEALS, CHANNEL_HEALS, DIRECT_HEALS, HOT_HEALS, CASTED_HEALS, ABSORB_SHIELDS, ALL_DATA, BOMB_HEALS
+
+local COMM_PREFIX = "LHC40"
+local playerGUID, playerName, playerLevel
+local playerHealModifier = 1
+local IS_BUILD30300 = tonumber((select(4, GetBuildInfo()))) >= 30300
+
+HealComm.callbacks = HealComm.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(HealComm)
+HealComm.spellData = HealComm.spellData or {}
+HealComm.hotData = HealComm.hotData or {}
+HealComm.talentData = HealComm.talentData or {}
+HealComm.itemSetsData = HealComm.itemSetsData or {}
+HealComm.glyphCache = HealComm.glyphCache or {}
+HealComm.equippedSetCache = HealComm.equippedSetCache or {}
+HealComm.guidToGroup = HealComm.guidToGroup or {}
+HealComm.guidToUnit = HealComm.guidToUnit or {}
+HealComm.pendingHeals = HealComm.pendingHeals or {}
+HealComm.tempPlayerList = HealComm.tempPlayerList or {}
+HealComm.activePets = HealComm.activePets or {}
+HealComm.activeHots = HealComm.activeHots or {}
+
+if( not HealComm.unitToPet ) then
+ HealComm.unitToPet = {["player"] = "pet"}
+ for i=1, MAX_PARTY_MEMBERS do HealComm.unitToPet["party" .. i] = "partypet" .. i end
+ for i=1, MAX_RAID_MEMBERS do HealComm.unitToPet["raid" .. i] = "raidpet" .. i end
+ for i=1, 5 do HealComm.unitToPet["arena" .. i] = "arenapet" .. i end
+end
+
+local spellData, hotData, tempPlayerList, pendingHeals = HealComm.spellData, HealComm.hotData, HealComm.tempPlayerList, HealComm.pendingHeals
+local equippedSetCache, itemSetsData, talentData = HealComm.equippedSetCache, HealComm.itemSetsData, HealComm.talentData
+local activeHots, activePets = HealComm.activeHots, HealComm.activePets
+
+-- Figure out what they are now since a few things change based off of this
+local playerClass = select(2, UnitClass("player"))
+local isHealerClass = playerClass == "DRUID" or playerClass == "PRIEST" or playerClass == "SHAMAN" or playerClass == "PALADIN"
+
+-- Stolen from Threat-2.0, compresses GUIDs from 18 characters to around 8 - 9, 50%/55% savings
+-- 44 = , / 58 = : / 255 = \255 / 0 = line break / 64 = @ / 254 = FE, used for escape code so has to be escaped
+if( not HealComm.compressGUID or not HealComm.fixedCompress ) then
+ local map = {[58] = "\254\250", [64] = "\254\251", [44] = "\254\252", [255] = "\254\253", [0] = "\255", [254] = "\254\249"}
+ local function guidCompressHelper(x)
+ local a = tonumber(x, 16)
+ return map[a] or char(a)
+ end
+
+ local dfmt = "0x%02X%02X%02X%02X%02X%02X%02X%02X"
+ local function unescape(str)
+ str = gsub(str, "\255", "\000")
+ str = gsub(str, "\254\250", "\058")
+ str = gsub(str, "\254\251", "\064")
+ str = gsub(str, "\254\252", "\044")
+ str = gsub(str, "\254\253", "\255")
+ return gsub(str, "\254\249", "\254")
+ end
+
+ HealComm.fixedCompress = true
+ HealComm.compressGUID = setmetatable({}, {
+ __index = function(tbl, guid)
+ local cguid = match(guid, "0x(.*)")
+ local str = gsub(cguid, "(%x%x)", guidCompressHelper)
+
+ rawset(tbl, guid, str)
+ return str
+ end})
+
+ HealComm.decompressGUID = setmetatable({}, {
+ __index = function(tbl, str)
+ if( not str ) then return nil end
+ local usc = unescape(str)
+ local a, b, c, d, e, f, g, h = byte(usc, 1, 8)
+
+ -- Failed to decompress, silently exit
+ if( not a or not b or not c or not d or not e or not f or not g or not h ) then
+ return ""
+ end
+
+ local guid = format(dfmt, a, b, c, d, e, f, g, h)
+
+ rawset(tbl, str, guid)
+ return guid
+ end})
+end
+
+local compressGUID, decompressGUID = HealComm.compressGUID, HealComm.decompressGUID
+
+-- Handles caching of tables for variable tick spells, like Wild Growth
+if( not HealComm.tableCache ) then
+ HealComm.tableCache = setmetatable({}, {__mode = "k"})
+ function HealComm:RetrieveTable()
+ return tremove(HealComm.tableCache, 1) or {}
+ end
+
+ function HealComm:DeleteTable(tbl)
+ twipe(tbl)
+ tinsert(HealComm.tableCache, tbl)
+ end
+end
+
+-- Validation for passed arguments
+if( not HealComm.tooltip ) then
+ local tooltip = CreateFrame("GameTooltip")
+ tooltip:SetOwner(UIParent, "ANCHOR_NONE")
+ tooltip.TextLeft1 = tooltip:CreateFontString()
+ tooltip.TextRight1 = tooltip:CreateFontString()
+ tooltip:AddFontStrings(tooltip.TextLeft1, tooltip.TextRight1)
+
+ HealComm.tooltip = tooltip
+end
+
+-- So I don't have to keep matching the same numbers every time or create a local copy of every rank -> # map for locals
+if( not HealComm.rankNumbers ) then
+ HealComm.rankNumbers = setmetatable({}, {
+ __index = function(tbl, index)
+ local number = tonumber(match(index, "(%d+)")) or 1
+
+ rawset(tbl, index, number)
+ return number
+ end,
+ })
+end
+
+-- Find the spellID by the name/rank combination
+-- Need to swap this to a double table metatable something like [spellName][spellRank] so I can reduce the garbage created
+if( not HealComm.spellToID ) then
+ HealComm.spellToID = setmetatable({}, {
+ __index = function(tbl, index)
+ -- Find the spell from the spell book and cache the results!
+ local offset, numSpells = select(3, GetSpellTabInfo(GetNumSpellTabs()))
+ for id=1, (offset + numSpells) do
+ -- Match, yay!
+ local spellName, spellRank = GetSpellName(id, BOOKTYPE_SPELL)
+ local name = spellName .. spellRank
+ if( index == name ) then
+ HealComm.tooltip:SetSpell(id, BOOKTYPE_SPELL)
+ local spellID = select(3, HealComm.tooltip:GetSpell())
+ if( spellID ) then
+ rawset(tbl, index, spellID)
+ return spellID
+ end
+ end
+ end
+
+ rawset(tbl, index, false)
+ return false
+ end,
+ })
+end
+
+-- This gets filled out after data has been loaded, this is only for casted heals. Hots just directly pull from the averages as they do not increase in power with level, Cataclysm will change this though.
+if( HealComm.averageHealMT and not HealComm.fixedAverage ) then
+ HealComm.averageHealMT = nil
+end
+
+HealComm.fixedAverage = true
+HealComm.averageHeal = HealComm.averageHeal or {}
+HealComm.averageHealMT = HealComm.averageHealMT or {
+ __index = function(tbl, index)
+ local rank = HealComm.rankNumbers[index]
+ local spellData = HealComm.spellData[rawget(tbl, "spell")]
+ local spellLevel = spellData.levels[rank]
+
+ -- No increase, it doesn't scale with levely
+ if( not spellData.increase or UnitLevel("player") <= spellLevel ) then
+ rawset(tbl, index, spellData.averages[rank])
+ return spellData.averages[rank]
+ end
+
+ local average = spellData.averages[rank]
+ if( UnitLevel("level") >= MAX_PLAYER_LEVEL ) then
+ average = average + spellData.increase[rank]
+ -- Here's how this works: If a spell increases 1,000 between 70 and 80, the player is level 75 the spell is 70
+ -- it's 1000 / (80 - 70) so 100, the player learned the spell 5 levels ago which means that the spell average increases by 500
+ -- This figures out how much it increases per level and how ahead of the spells level they are to figure out how much to add
+ else
+ average = average + (UnitLevel("player") - spellLevel) * (spellData.increase[rank] / (MAX_PLAYER_LEVEL - spellLevel))
+ end
+
+ rawset(tbl, index, average)
+ return average
+ end}
+
+-- Record management, because this is getting more complicted to deal with
+local function updateRecord(pending, guid, amount, stack, endTime, ticksLeft)
+ if( pending[guid] ) then
+ local id = pending[guid]
+
+ pending[id] = guid
+ pending[id + 1] = amount
+ pending[id + 2] = stack
+ pending[id + 3] = endTime or 0
+ pending[id + 4] = ticksLeft or 0
+ else
+ pending[guid] = #(pending) + 1
+ tinsert(pending, guid)
+ tinsert(pending, amount)
+ tinsert(pending, stack)
+ tinsert(pending, endTime or 0)
+ tinsert(pending, ticksLeft or 0)
+
+ if( pending.bitType == HOT_HEALS ) then
+ activeHots[guid] = (activeHots[guid] or 0) + 1
+ HealComm.hotMonitor:Show()
+ end
+ end
+end
+
+local function getRecord(pending, guid)
+ local id = pending[guid]
+ if( not id ) then return nil end
+
+ -- amount, stack, endTime, ticksLeft
+ return pending[id + 1], pending[id + 2], pending[id + 3], pending[id + 4]
+end
+
+local function removeRecord(pending, guid)
+ local id = pending[guid]
+ if( not id ) then return nil end
+
+ -- ticksLeft, endTime, stack, amount, guid
+ tremove(pending, id + 4)
+ tremove(pending, id + 3)
+ tremove(pending, id + 2)
+ local amount = tremove(pending, id + 1)
+ tremove(pending, id)
+ pending[guid] = nil
+
+ -- Release the table
+ if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
+
+ if( pending.bitType == HOT_HEALS and activeHots[guid] ) then
+ activeHots[guid] = activeHots[guid] - 1
+ activeHots[guid] = activeHots[guid] > 0 and activeHots[guid] or nil
+ end
+
+ -- Shift any records after this ones index down 5 to account for the removal
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ if( pending[guid] > id ) then
+ pending[guid] = pending[guid] - 5
+ end
+ end
+end
+
+local function removeRecordList(pending, inc, comp, ...)
+ for i=1, select("#", ...), inc do
+ local guid = select(i, ...)
+ guid = comp and decompressGUID[guid] or guid
+
+ local id = pending[guid]
+ -- ticksLeft, endTime, stack, amount, guid
+ tremove(pending, id + 4)
+ tremove(pending, id + 3)
+ tremove(pending, id + 2)
+ local amount = tremove(pending, id + 1)
+ tremove(pending, id)
+ pending[guid] = nil
+
+ -- Release the table
+ if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
+ end
+
+ -- Redo all the id maps
+ for i=1, #(pending), 5 do
+ pending[pending[i]] = i
+ end
+end
+
+-- Removes every mention to the given GUID
+local function removeAllRecords(guid)
+ local changed
+ for _, spells in pairs(pendingHeals) do
+ for _, pending in pairs(spells) do
+ if( pending.bitType and pending[guid] ) then
+ local id = pending[guid]
+
+ -- ticksLeft, endTime, stack, amount, guid
+ tremove(pending, id + 4)
+ tremove(pending, id + 3)
+ tremove(pending, id + 2)
+ local amount = tremove(pending, id + 1)
+ tremove(pending, id)
+ pending[guid] = nil
+
+ -- Release the table
+ if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
+
+ -- Shift everything back
+ if( #(pending) > 0 ) then
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ if( pending[guid] > id ) then
+ pending[guid] = pending[guid] - 5
+ end
+ end
+ else
+ twipe(pending)
+ end
+
+ changed = true
+ end
+ end
+ end
+
+ activeHots[guid] = nil
+
+ if( changed ) then
+ HealComm.callbacks:Fire("HealCommArena_GUIDDisappeared", guid)
+ end
+end
+
+-- These are not public APIs and are purely for the wrapper to use
+HealComm.removeRecordList = removeRecordList
+HealComm.removeRecord = removeRecord
+HealComm.getRecord = getRecord
+HealComm.updateRecord = updateRecord
+
+-- Removes all pending heals, if it's a group that is causing the clear then we won't remove the players heals on themselves
+local function clearPendingHeals()
+ for casterGUID, spells in pairs(pendingHeals) do
+ for _, pending in pairs(spells) do
+ if( pending.bitType ) then
+ twipe(tempPlayerList)
+ for i=#(pending), 1, -5 do tinsert(tempPlayerList, pending[i - 4]) end
+
+ if( #(tempPlayerList) > 0 ) then
+ local spellID, bitType = pending.spellID, pending.bitType
+ twipe(pending)
+
+ HealComm.callbacks:Fire("HealCommArena_HealStopped", casterGUID, spellID, bitType, true, unpack(tempPlayerList))
+ end
+ end
+ end
+ end
+end
+
+-- APIs
+-- Returns the players current heaing modifier
+function HealComm:GetPlayerHealingMod()
+ return playerHealModifier or 1
+end
+
+-- Returns the current healing modifier for the GUID
+function HealComm:GetHealModifier(guid)
+ return HealComm.currentModifiers[guid] or 1
+end
+
+-- Returns whether or not the GUID has casted a heal
+function HealComm:GUIDHasHealed(guid)
+ return pendingHeals[guid] and true or nil
+end
+
+-- Returns the guid to unit table
+function HealComm:GetGUIDUnitMapTable()
+ if( not HealComm.protectedMap ) then
+ HealComm.protectedMap = setmetatable({}, {
+ __index = function(tbl, key) return HealComm.guidToUnit[key] end,
+ __newindex = function() error("This is a read only table and cannot be modified.", 2) end,
+ __metatable = false
+ })
+ end
+
+ return HealComm.protectedMap
+end
+
+-- Gets the next heal landing on someone using the passed filters
+function HealComm:GetNextHealAmount(guid, bitFlag, time, ignoreGUID)
+ local healTime, healAmount, healFrom
+ local currentTime = GetTime()
+
+ for casterGUID, spells in pairs(pendingHeals) do
+ if( not ignoreGUID or ignoreGUID ~= casterGUID ) then
+ for _, pending in pairs(spells) do
+ if( pending.bitType and band(pending.bitType, bitFlag) > 0 ) then
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ local amount = pending[i + 1]
+ local stack = pending[i + 2]
+ local endTime = pending[i + 3]
+ endTime = endTime > 0 and endTime or pending.endTime
+
+ -- Direct heals are easy, if they match the filter then return them
+ if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
+ if( not healTime or endTime < healTime ) then
+ healTime = endTime
+ healAmount = amount * stack
+ healFrom = casterGUID
+ end
+
+ -- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
+ elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and ( not pending.hasVariableTicks or pending.hasVariableTicks and amount[1] ) ) then
+ local secondsLeft = time and time - currentTime or endTime - currentTime
+ local nextTick = currentTime + (secondsLeft % pending.tickInterval)
+ if( not healTime or nextTick < healTime ) then
+ healTime = nextTick
+ healAmount = not pending.hasVariableTicks and amount * stack or amount[1] * stack
+ healFrom = casterGUID
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return healTime, healFrom, healAmount
+end
+
+-- Get the healing amount that matches the passed filters
+local function filterData(spells, filterGUID, bitFlag, time, ignoreGUID)
+ local healAmount = 0
+ local currentTime = GetTime()
+
+ for _, pending in pairs(spells) do
+ if( pending.bitType and band(pending.bitType, bitFlag) > 0 ) then
+ for i=1, #(pending), 5 do
+ local guid = pending[i]
+ if( guid == filterGUID or ignoreGUID ) then
+ local amount = pending[i + 1]
+ local stack = pending[i + 2]
+ local endTime = pending[i + 3]
+ endTime = endTime > 0 and endTime or pending.endTime
+
+ -- Direct heals are easy, if they match the filter then return them
+ if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
+ healAmount = healAmount + amount * stack
+ -- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
+ elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and endTime > currentTime ) then
+ local ticksLeft = pending[i + 4]
+ if( not time or time >= endTime ) then
+ if( not pending.hasVariableTicks ) then
+ healAmount = healAmount + (amount * stack) * ticksLeft
+ else
+ for _, heal in pairs(amount) do
+ healAmount = healAmount + (heal * stack)
+ end
+ end
+ else
+ local secondsLeft = endTime - currentTime
+ local bandSeconds = time - currentTime
+ local ticks = floor(min(bandSeconds, secondsLeft) / pending.tickInterval)
+ local nextTickIn = secondsLeft % pending.tickInterval
+ local fractionalBand = bandSeconds % pending.tickInterval
+ if( nextTickIn > 0 and nextTickIn < fractionalBand ) then
+ ticks = ticks + 1
+ end
+
+ if( not pending.hasVariableTicks ) then
+ healAmount = healAmount + (amount * stack) * min(ticks, ticksLeft)
+ else
+ for j=1, min(ticks, #(amount)) do
+ healAmount = healAmount + (amount[j] * stack)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return healAmount
+end
+
+-- Gets healing amount using the passed filters
+function HealComm:GetHealAmount(guid, bitFlag, time, casterGUID)
+ local amount = 0
+ if( casterGUID and pendingHeals[casterGUID] ) then
+ amount = filterData(pendingHeals[casterGUID], guid, bitFlag, time)
+ elseif( not casterGUID ) then
+ for _, spells in pairs(pendingHeals) do
+ amount = amount + filterData(spells, guid, bitFlag, time)
+ end
+ end
+
+ return amount > 0 and amount or nil
+end
+
+-- Gets healing amounts for everyone except the player using the passed filters
+function HealComm:GetOthersHealAmount(guid, bitFlag, time)
+ local amount = 0
+ for casterGUID, spells in pairs(pendingHeals) do
+ if( casterGUID ~= playerGUID ) then
+ amount = amount + filterData(spells, guid, bitFlag, time)
+ end
+ end
+
+ return amount > 0 and amount or nil
+end
+
+function HealComm:GetCasterHealAmount(guid, bitFlag, time)
+ local amount = pendingHeals[guid] and filterData(pendingHeals[guid], nil, bitFlag, time, true) or 0
+ return amount > 0 and amount or nil
+end
+
+-- Healing class data
+-- Thanks to Gagorian (DrDamage) for letting me steal his formulas and such
+local playerCurrentRelic
+local averageHeal, rankNumbers = HealComm.averageHeal, HealComm.rankNumbers
+local guidToUnit, guidToGroup, glyphCache = HealComm.guidToUnit, HealComm.guidToGroup, HealComm.glyphCache
+
+-- UnitBuff priortizes our buffs over everyone elses when there is a name conflict, so yay for that
+local function unitHasAura(unit, name)
+ return select(8, UnitBuff(unit, name)) == "player"
+end
+
+-- Note because I always forget on the order:
+-- Talents that effective the coeffiency of spell power to healing are first and are tacked directly onto the coeffiency (Empowered Rejuvenation)
+-- Penalty modifiers (downranking/spell level too low) are applied directly to the spell power
+-- Spell power modifiers are then applied to the spell power
+-- Heal modifiers are applied after all of that
+-- Crit modifiers are applied after
+-- Any other modifiers such as Mortal Strike or Avenging Wrath are applied after everything else
+local function calculateGeneralAmount(level, amount, spellPower, spModifier, healModifier)
+ -- Apply downranking penalities for spells below 20
+ local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
+
+ -- Apply further downranking penalities
+ spellPower = spellPower * (penalty * min(1, max(0, 1 - (playerLevel - level - 11) * 0.05)))
+
+ -- Apply zone modifier
+ healModifier = healModifier * HealComm.zoneHealModifier
+
+ -- Do the general factoring
+ return healModifier * (amount + (spellPower * spModifier))
+end
+
+-- For spells like Wild Growth, it's a waste to do the calculations for each tick, easier to calculate spell power now and then manually calculate it all after
+local function calculateSpellPower(level, spellPower)
+ -- Apply downranking penalities for spells below 20
+ local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
+
+ -- Apply further downranking penalities
+ return spellPower * (penalty * min(1, max(0, 1 - (playerLevel - level - 11) * 0.05)))
+end
+
+-- Yes silly function, just cleaner to look at
+local function avg(a, b)
+ return (a + b) / 2
+end
+
+--[[
+ What the different callbacks do:
+
+ AuraHandler: Specific aura tracking needed for this class, who has Beacon up on them and such
+
+ ResetChargeData: Due to spell "queuing" you can't always rely on aura data for buffs that last one or two casts, for example Divine Favor (+100% crit, one spell)
+ if you cast Holy Light and queue Flash of Light the library would still see they have Divine Favor and give them crits on both spells. The reset means that the flag that indicates
+ they have the aura can be killed and if they interrupt the cast then it will call this and let you reset the flags.
+
+ What happens in terms of what the client thinks and what actually is, is something like this:
+
+ UNIT_SPELLCAST_START, Holy Light -> Divine Favor up
+ UNIT_SPELLCAST_SUCCEEDED, Holy Light -> Divine Favor up (But it was really used)
+ UNIT_SPELLCAST_START, Flash of Light -> Divine Favor up (It's not actually up but auras didn't update)
+ UNIT_AURA -> Divine Favor up (Split second where it still thinks it's up)
+ UNIT_AURA -> Divine Favor faded (Client catches up and realizes it's down)
+
+ CalculateHealing: Calculates the healing value, does all the formula calculations talent modifiers and such
+
+ CalculateHotHealing: Used specifically for calculating the heals of hots
+
+ GetHealTargets: Who the heal is going to hit, used for setting extra targets for Beacon of Light + Paladin heal or Prayer of Healing.
+ The returns should either be:
+
+ "compressedGUID1,compressedGUID2,compressedGUID3,compressedGUID4", healthAmount
+ Or if you need to set specific healing values for one GUID it should be
+ "compressedGUID1,healthAmount1,compressedGUID2,healAmount2,compressedGUID3,healAmount3", -1
+
+ The latter is for cases like Glyph of Healing Wave where you need a heal for 1,000 on A and a heal for 200 on the player for B without sending 2 events.
+ The -1 tells the library to look in the GUId list for the heal amounts
+
+ **NOTE** Any GUID returned from GetHealTargets must be compressed through a call to compressGUID[guid]
+]]
+
+local CalculateHealing, GetHealTargets, AuraHandler, CalculateHotHealing, ResetChargeData, LoadClassData
+
+-- DRUIDS
+-- All data is accurate as of 3.2.2 (build 10392)
+if( playerClass == "DRUID" ) then
+ LoadClassData = function()
+ -- Rejuvenation
+ local Rejuvenation = GetSpellInfo(774)
+ hotData[Rejuvenation] = {interval = 3,
+ levels = {4, 10, 16, 22, 28, 34, 40, 46, 52, 58, 60, 63, 69, 75, 80}, averages = {32, 56, 116, 180, 244, 304, 388, 488, 608, 756, 888, 932, 1060, 1192, 1690}}
+ -- Regrowth
+ local Regrowth = GetSpellInfo(8936)
+ hotData[Regrowth] = {interval = 3, ticks = 7, coeff = 1.316,
+ levels = {12, 18, 24, 30, 36, 42, 48, 54, 60, 65, 71, 77}, averages = {98, 175, 259, 343, 427, 546, 686, 861, 1064, 1274, 1792, 2345}}
+ -- Lifebloom
+ local Lifebloom = GetSpellInfo(33763)
+ hotData[Lifebloom] = {interval = 1, ticks = 7, coeff = 0.66626, dhCoeff = 0.34324 * 0.8, levels = {64, 72, 80}, averages = {224, 287, 371}, bomb = {480, 616, 776}}
+ -- Wild Growth
+ local WildGrowth = GetSpellInfo(48438)
+ hotData[WildGrowth] = {interval = 1, ticks = 7, coeff = 0.8056, levels = {60, 70, 75, 80}, averages = {686, 861, 1239, 1442}}
+
+ -- Regrowth
+ spellData[Regrowth] = {coeff = 0.2867,
+ levels = hotData[Regrowth].levels,
+ averages = {avg(84, 98), avg(164, 188), avg(240, 274), avg(318, 360), avg(405, 457), avg(511, 575), avg(646, 724), avg(809, 905), avg(1003, 1119), avg(1215, 1355), avg(1710, 1908), avg(2234, 2494)},
+ increase = {122, 155, 173, 180, 180, 178, 169, 156, 136, 115, 97, 23}}
+ -- Healing Touch
+ local HealingTouch = GetSpellInfo(5185)
+ spellData[HealingTouch] = {
+ levels = {1, 8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 62, 69, 74, 79},
+ averages = {avg(37, 51), avg(88, 112), avg(195, 243), avg(363, 445), avg(490, 594), avg(636, 766), avg(802, 960), avg(1199, 1427), avg(1299, 1539), avg(1620, 1912), avg(1944, 2294), avg(2026, 2392), avg(2321, 2739), avg(3223, 3805), avg(3750, 4428)}}
+ -- Nourish
+ local Nourish = GetSpellInfo(50464)
+ spellData[Nourish] = {coeff = 0.358005, levels = {80}, averages = {avg(1883, 2187)}}
+ -- Tranquility
+ local Tranquility = GetSpellInfo(740)
+ spellData[Tranquility] = {coeff = 1.144681, ticks = 4, levels = {30, 40, 50, 60, 70, 75, 80}, averages = {351, 515, 765, 1097, 1518, 2598, 3035}}
+
+ -- Talent data, these are filled in later and modified on talent changes
+ -- Master Shapeshifter (Multi)
+ local MasterShapeshifter = GetSpellInfo(48411)
+ talentData[MasterShapeshifter] = {mod = 0.02, current = 0}
+ -- Gift of Nature (Add)
+ local GiftofNature = GetSpellInfo(17104)
+ talentData[GiftofNature] = {mod = 0.02, current = 0}
+ -- Empowered Touch (Add, increases spell power HT/Nourish gains)
+ local EmpoweredTouch = GetSpellInfo(33879)
+ talentData[EmpoweredTouch] = {mod = 0.2, current = 0}
+ -- Empowered Rejuvenation (Multi, this ups both the direct heal and the hot)
+ local EmpoweredRejuv = GetSpellInfo(33886)
+ talentData[EmpoweredRejuv] = {mod = 0.04, current = 0}
+ -- Genesis (Add)
+ local Genesis = GetSpellInfo(57810)
+ talentData[Genesis] = {mod = 0.01, current = 0}
+ -- Improved Rejuvenation (Add)
+ local ImprovedRejuv = GetSpellInfo(17111)
+ talentData[ImprovedRejuv] = {mod = 0.05, current = 0}
+ -- Nature's Splendor (+3s Rejuv/+6s Regrowth/+2s Lifebloom)
+ local NaturesSplendor = GetSpellInfo(57865)
+ talentData[NaturesSplendor] = {mod = 1, current = 0}
+
+ local TreeofLife = GetSpellInfo(33891)
+ local Innervate = GetSpellInfo(29166)
+
+ -- Set data
+ -- 2 piece, +6 seconds to Regrowth
+ itemSetsData["T5 Resto"] = {30216, 30217, 30219, 30220, 30221}
+ -- +5% more healing to Nourish per hot
+ itemSetsData["T7 Resto"] = {40460, 40461, 40462, 40463, 40465, 39531, 39538, 39539, 39542, 39543}
+ --itemSetsData["T8 Resto"] = {46183, 46184, 46185, 46186, 46187, 45345, 45346, 45347, 45348, 45349}
+ --itemSetsData["T9 Resto"] = {48102, 48129, 48130, 48131, 48132, 48153, 48154, 48155, 48156, 48157, 48133, 48134, 48135, 48136, 48137, 48142, 48141, 48140, 48139, 48138, 48152, 48151, 48150, 48149, 48148, 48143, 48144, 48145, 48146, 48147}
+ -- 2 piece, 30% less healing lost on WG
+ itemSetsData["T10 Resto"] = {50106, 50107, 50108, 50109, 50113, 51139, 51138, 51137, 51136, 51135, 51300, 51301, 51302, 51303, 51304}
+
+ local bloomBombIdols = {[28355] = 87, [33076] = 105, [33841] = 116, [35021] = 131, [42576] = 188, [42577] = 217, [42578] = 246, [42579] = 294, [42580] = 376, [51423] = 448}
+
+ local hotTotals, hasRegrowth = {}, {}
+ AuraHandler = function(unit, guid)
+ hotTotals[guid] = 0
+ if( unitHasAura(unit, Rejuvenation) ) then hotTotals[guid] = hotTotals[guid] + 1 end
+ if( unitHasAura(unit, Lifebloom) ) then hotTotals[guid] = hotTotals[guid] + 1 end
+ if( unitHasAura(unit, WildGrowth) ) then hotTotals[guid] = hotTotals[guid] + 1 end
+ if( unitHasAura(unit, Regrowth) ) then
+ hasRegrowth[guid] = true
+ hotTotals[guid] = hotTotals[guid] + 1
+ else
+ hasRegrowth[guid] = nil
+ end
+ end
+
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ -- Tranquility pulses on everyone within 30 yards, if they are in range of Innervate they'll get Tranquility
+ if( spellName == Tranquility ) then
+ local targets = compressGUID[playerGUID]
+ local playerGroup = guidToGroup[playerGUID]
+
+ for groupGUID, groupID in pairs(guidToGroup) do
+ if( groupID == playerGroup and playerGUID ~= groupGUID and not UnitHasVehicleUI(guidToUnit[groupID]) and IsSpellInRange(Innervate, guidToUnit[groupGUID]) == 1 ) then
+ targets = targets .. "," .. compressGUID[groupGUID]
+ end
+ end
+
+ return targets, healAmount
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ -- Calculate hot heals
+ local wgTicks = {}
+ CalculateHotHealing = function(guid, spellID)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ local rank = HealComm.rankNumbers[spellRank]
+ local healAmount = hotData[spellName].averages[rank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local bombAmount, totalTicks
+ healModifier = healModifier + talentData[GiftofNature].current
+ healModifier = healModifier + talentData[Genesis].current
+
+ -- Master Shapeshifter does not apply directly when using Lifebloom
+ if( unitHasAura("player", TreeofLife) ) then
+ healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
+
+ -- 32387 - Idol of the Raven Godess, +44 SP while in TOL
+ if( playerCurrentRelic == 32387 ) then
+ spellPower = spellPower + 44
+ end
+ end
+
+ -- Rejuvenation
+ if( spellName == Rejuvenation ) then
+ healModifier = healModifier + talentData[ImprovedRejuv].current
+
+ -- 25643 - Harold's Rejuvenation Broach, +86 Rejuv SP
+ if( playerCurrentRelic == 25643 ) then
+ spellPower = spellPower + 86
+ -- 22398 - Idol of Rejuvenation, +50 SP to Rejuv
+ elseif( playerCurrentRelic == 22398 ) then
+ spellPower = spellPower + 50
+ end
+
+ local duration, ticks
+ if( IS_BUILD30300 ) then
+ duration = 15
+ ticks = 5
+ totalTicks = 5
+ else
+ duration = rank > 14 and 15 or 12
+ ticks = duration / hotData[spellName].interval
+ totalTicks = ticks
+ end
+
+ spellPower = spellPower * (((duration / 15) * 1.88) * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / ticks
+ healAmount = healAmount / ticks
+
+ --38366 - Idol of Pure Thoughts, +33 SP base per tick
+ if( playerCurrentRelic == 38366 ) then
+ spellPower = spellPower + 33
+ end
+
+ -- Nature's Splendor, +6 seconds
+ if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
+
+ -- Regrowth
+ elseif( spellName == Regrowth ) then
+ spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ totalTicks = 7
+ -- Nature's Splendor, +6 seconds
+ if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 2 end
+ -- T5 Resto, +6 seconds
+ if( equippedSetCache["T5 Resto"] >= 2 ) then totalTicks = totalTicks + 2 end
+
+ -- Lifebloom
+ elseif( spellName == Lifebloom ) then
+ -- Figure out the bomb heal, apparently Gift of Nature double dips and will heal 10% for the HOT + 10% again for the direct heal
+ local bombSpellPower = spellPower
+ if( playerCurrentRelic and bloomBombIdols[playerCurrentRelic] ) then
+ bombSpellPower = bombSpellPower + bloomBombIdols[playerCurrentRelic]
+ end
+
+ local bombSpell = bombSpellPower * (hotData[spellName].dhCoeff * 1.88)
+ bombAmount = ceil(calculateGeneralAmount(hotData[spellName].levels[rank], hotData[spellName].bomb[rank], bombSpell, spModifier, healModifier + talentData[GiftofNature].current))
+
+ -- Figure out the hot tick healing
+ spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+ -- Figure out total ticks
+ totalTicks = 7
+
+ -- Idol of Lush Moss, +125 SP per tick
+ if( playerCurrentRelic == 40711 ) then
+ spellPower = spellPower + 125
+ -- Idol of the Emerald Queen, +47 SP per tick
+ elseif( playerCurrentRelic == 27886 ) then
+ spellPower = spellPower + 47
+ end
+
+ -- Glyph of Lifebloom, +1 second
+ if( glyphCache[54826] ) then totalTicks = totalTicks + 1 end
+ -- Nature's Splendor, +2 seconds
+ if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
+ -- Wild Growth
+ elseif( spellName == WildGrowth ) then
+ spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / hotData[spellName].ticks
+ spellPower = calculateSpellPower(hotData[spellName].levels[rank], spellPower)
+ healAmount = healAmount / hotData[spellName].ticks
+ healModifier = healModifier * HealComm.zoneHealModifier
+
+ twipe(wgTicks)
+ local tickModifier = equippedSetCache["T10 Resto"] >= 2 and 0.70 or 1
+ local tickAmount = healAmount / hotData[spellName].ticks
+ for i=1, hotData[spellName].ticks do
+ tinsert(wgTicks, ceil(healModifier * ((healAmount + tickAmount * (3 - (i - 1) * tickModifier)) + (spellPower * spModifier))))
+ end
+
+ return HOT_HEALS, wgTicks, hotData[spellName].ticks, hotData[spellName].interval, nil, true
+ end
+
+ healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+ return HOT_HEALS, ceil(healAmount), totalTicks, hotData[spellName].interval, bombAmount
+ end
+
+ -- Calcualte direct and channeled heals
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local rank = HealComm.rankNumbers[spellRank]
+
+ -- Gift of Nature
+ healModifier = healModifier + talentData[GiftofNature].current
+
+ -- Master Shapeshifter does not apply directly when using Lifebloom
+ if( unitHasAura("player", TreeofLife) ) then
+ healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
+
+ -- 32387 - Idol of the Raven Godess, +44 SP while in TOL
+ if( playerCurrentRelic == 32387 ) then
+ spellPower = spellPower + 44
+ end
+ end
+
+ -- Regrowth
+ if( spellName == Regrowth ) then
+ -- Glyph of Regrowth - +20% if target has Regrowth
+ if( glyphCache[54743] and hasRegrowth[guid] ) then
+ healModifier = healModifier * 1.20
+ end
+
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
+ -- Nourish
+ elseif( spellName == Nourish ) then
+ -- 46138 - Idol of Flourishing Life, +187 Nourish SP
+ if( playerCurrentRelic == 46138 ) then
+ spellPower = spellPower + 187
+ end
+
+ -- Apply any hot specific bonuses
+ local hots = hotTotals[guid]
+ if( hots and hots > 0 ) then
+ local bonus = 1.20
+
+ -- T7 Resto, +5% healing per each of the players hot on their target
+ if( equippedSetCache["T7 Resto"] >= 2 ) then
+ bonus = bonus + 0.05 * hots
+ end
+
+ -- Glyph of Nourish - 6% per HoT
+ if( glyphCache[62971] ) then
+ bonus = bonus + 0.06 * hots
+ end
+
+ healModifier = healModifier * bonus
+ end
+
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[EmpoweredTouch].spent * 0.10)
+ -- Healing Touch
+ elseif( spellName == HealingTouch ) then
+ -- Glyph of Healing Touch, -50% healing
+ if( glyphCache[54825] ) then
+ healModifier = healModifier - 0.50
+ end
+
+ -- Idol of the Avian Heart, +136 baseh ealing
+ if( playerCurrentRelic == 28568 ) then
+ healAmount = healAmount + 136
+ -- Idol of Health, +100 base healing
+ elseif( playerCurrentRelic == 22399 ) then
+ healAmount = healAmount + 100
+ end
+
+ -- Rank 1 - 3: 1.5/2/2.5 cast time, Rank 4+: 3 cast time
+ local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
+ spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[EmpoweredTouch].current)
+
+ -- Tranquility
+ elseif( spellName == Tranquility ) then
+ healModifier = healModifier + talentData[Genesis].current
+
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
+ spellPower = spellPower / spellData[spellName].ticks
+ end
+
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- 100% chance to crit with Nature, this mostly just covers fights like Loatheb where you will basically have 100% crit
+ if( GetSpellCritChance(4) >= 100 ) then
+ healAmount = healAmount * 1.50
+ end
+
+ if( spellData[spellName].ticks ) then
+ return CHANNEL_HEALS, ceil(healAmount), spellData[spellName].ticks, spellData[spellName].ticks
+ end
+
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+-- PALADINS
+-- All data is accurate as of 3.2.2 (build 10392)
+if( playerClass == "PALADIN" ) then
+ LoadClassData = function()
+ -- Hot data, this is just so it realizes that FoL can be a hot so it will call the calculator
+ --local FlashofLight = GetSpellInfo(19750)
+ --hotData[FlashofLight] = true
+
+ -- Spell data
+ -- Holy Light
+ local HolyLight = GetSpellInfo(635)
+ spellData[HolyLight] = {coeff = 2.5 / 3.5 * 1.25,
+ levels = {1, 6, 14, 22, 30, 38, 46, 54, 60, 62, 70, 75, 80},
+ averages = {avg(50, 60), avg(96, 116), avg(203, 239), avg(397, 455), avg(628, 708), avg(894, 998), avg(1209, 1349), avg(1595, 1777), avg(2034, 2266), avg(2232, 2486), avg(2818, 3138), avg(4199, 4677), avg(4888, 5444)},
+ increase = {63, 81, 112, 139, 155, 159, 156, 135, 116, 115, 70, 52, 0}}
+ -- Flash of Light
+ local FlashofLight = GetSpellInfo(19750)
+ spellData[FlashofLight] = {coeff = 1.5 / 3.5 * 1.25,
+ levels = {20, 26, 34, 42, 50, 58, 66, 74, 79},
+ averages = {avg(81, 93), avg(124, 144), avg(189, 211), avg(256, 288), avg(346, 390), avg(445, 499), avg(588, 658), avg(682, 764), avg(785, 879)},
+ increase = {60, 70, 73, 72, 66, 57, 42, 20, 3}}
+
+ -- Talent data
+ -- Need to figure out a way of supporting +6% healing from imp devo aura, might not be able to
+ -- Healing Light (Add)
+ local HealingLight = GetSpellInfo(20237)
+ talentData[HealingLight] = {mod = 0.04, current = 0}
+ -- Divinity (Add)
+ local Divinity = GetSpellInfo(63646)
+ talentData[Divinity] = {mod = 0.01, current = 0}
+ -- Touched by the Light (Add?)
+ local TouchedbytheLight = GetSpellInfo(53592)
+ talentData[TouchedbytheLight] = {mod = 0.10, current = 0}
+ -- 100% of your heal on someone within range of your beacon heals the beacon target too
+ local BeaconofLight = GetSpellInfo(53563)
+ -- 100% chance to crit
+ local DivineFavor = GetSpellInfo(20216)
+ -- Seal of Light + Glyph = 5% healing
+ local SealofLight = GetSpellInfo(20165)
+ -- Divine Illumination, used in T10 holy
+ local DivineIllumination = GetSpellInfo(31842)
+
+ local flashLibrams = {[42616] = 436, [42615] = 375, [42614] = 331, [42613] = 293, [42612] = 204, [28592] = 89, [25644] = 79, [23006] = 43, [23201] = 28}
+ local holyLibrams = {[45436] = 160, [40268] = 141, [28296] = 47}
+ local flashSPLibrams = {[51472] = 510}
+
+ -- Holy Shock crits put a hot that heals for 15% of the HS over 9s
+ --itemSetsData["T8 Holy"] = { 45370, 45371, 45372, 45373, 45374, 46178, 46179, 46180, 46181, 46182 }
+ -- +100% to the hot when using Flash of Light + Sacred Shield
+ --itemSetsData["T9 Holy"] = { 48595, 48596, 48597, 48598, 48599, 48564, 48566, 48568, 48572, 48574, 48593, 48591, 48592, 48590, 48594, 48588, 48586, 48587, 48585, 48589, 48576, 48578, 48577, 48579, 48575, 48583, 48581, 48582, 48580, 48584}
+ itemSetsData["T10 Holy"] = {50865, 50866, 50867, 50868, 50869, 51270, 51271, 51272, 51273, 51274, 51165, 51166, 51167, 51168, 51169}
+
+ -- Need the GUID of whoever has beacon on them so we can make sure they are visible to us and so we can check the mapping
+ local activeBeaconGUID, hasDivineFavor
+ AuraHandler = function(unit, guid)
+ if( unitHasAura(unit, BeaconofLight) ) then
+ activeBeaconGUID = guid
+ elseif( activeBeaconGUID == guid ) then
+ activeBeaconGUID = nil
+ end
+
+ -- Check Divine Favor
+ if( unit == "player" ) then
+ hasDivineFavor = unitHasAura("player", DivineFavor)
+ end
+ end
+
+ ResetChargeData = function(guid)
+ hasDivineFavor = unitHasAura("player", DivineFavor)
+ end
+
+ -- Check for beacon when figuring out who to heal
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ if( activeBeaconGUID and activeBeaconGUID ~= guid and guidToUnit[activeBeaconGUID] and UnitIsVisible(guidToUnit[activeBeaconGUID]) ) then
+ return format("%s,%s", compressGUID[guid], compressGUID[activeBeaconGUID]), healAmount
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ -- If only every other class was as easy as Paladins
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local rank = HealComm.rankNumbers[spellRank]
+
+ -- Glyph of Seal of Light, +5% healing if the player has Seal of Light up
+ if( glyphCache[54943] and unitHasAura("player", SealofLight) ) then
+ healModifier = healModifier + 0.05
+ end
+
+ healModifier = healModifier + talentData[HealingLight].current
+ healModifier = healModifier * (1 + talentData[Divinity].current)
+
+ -- Apply extra spell power based on libram
+ if( playerCurrentRelic ) then
+ if( spellName == HolyLight and holyLibrams[playerCurrentRelic] ) then
+ healAmount = healAmount + (holyLibrams[playerCurrentRelic] * 0.805)
+ elseif( spellName == FlashofLight and flashLibrams[playerCurrentRelic] ) then
+ healAmount = healAmount + (flashLibrams[playerCurrentRelic] * 0.805)
+ elseif( spellName == FlashofLight and flashSPLibrams[playerCurrentRelic] ) then
+ spellPower = spellPower + flashSPLibrams[playerCurrentRelic]
+ end
+ end
+
+ -- +35% healing while Divine Illumination is active
+ if( equippedSetCache["T10 Holy"] >= 2 and unitHasAura("player", DivineIllumination) ) then
+ healModifier = healModifier * 1.35
+ end
+
+ -- Normal calculations
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- Divine Favor, 100% chance to crit
+ -- ... or the player has over a 100% chance to crit with Holy spells
+ if( hasDivineFavor or GetSpellCritChance(2) >= 100 ) then
+ hasDivineFavor = nil
+ healAmount = healAmount * (1.50 * (1 + talentData[TouchedbytheLight].current))
+ end
+
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+-- PRIESTS
+-- Accurate as of 3.2.2 (build 10392)
+if( playerClass == "PRIEST" ) then
+ LoadClassData = function()
+ -- Hot data
+ local Renew = GetSpellInfo(139)
+ hotData[Renew] = {coeff = 1, interval = 3, ticks = 5, levels = {8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 65, 70, 75, 80}, averages = {45, 100, 175, 245, 315, 400, 510, 650, 810, 970, 1010, 1110, 1235, 1400}}
+ --local GlyphofPoH = GetSpellInfo(56161)
+ --hotData[GlyphofPoH] = {isMulti = true, interval = 3}
+
+ -- Spell data
+ -- Greater Heal
+ local GreaterHeal = GetSpellInfo(2060)
+ spellData[GreaterHeal] = {coeff = 3 / 3.5, levels = {40, 46, 52, 58, 60, 63, 68, 73, 78}, increase = {204, 197, 184, 165, 162, 142, 111, 92, 30},
+ averages = {avg(899, 1013), avg(1149, 1289), avg(1437, 1609), avg(1798, 2006), avg(1966, 2194), avg(2074, 2410), avg(2394, 2784), avg(3395, 3945), avg(3950, 4590)}}
+ -- Prayer of Healing
+ local PrayerofHealing = GetSpellInfo(596)
+ spellData[PrayerofHealing] = {coeff = 0.2798, levels = {30, 40, 50, 60, 60, 68, 76}, increase = {65, 64, 60, 48, 50, 33, 18},
+ averages = {avg(301, 321), avg(444, 472), avg(657, 695), avg(939, 991), avg(997, 1053), avg(1246, 1316), avg(2091, 2209)}}
+ -- Flash Heal
+ local FlashHeal = GetSpellInfo(2061)
+ spellData[FlashHeal] = {coeff = 1.5 / 3.5, levels = {20, 26, 32, 38, 44, 52, 58, 61, 67, 73, 79}, increase = {114, 118, 120, 117, 118, 111, 100, 89, 67, 56, 9},
+ averages = {avg(193, 237), avg(258, 314), avg(327, 393), avg(400, 478), avg(518, 616), avg(644, 764), avg(812, 958), avg(913, 1059), avg(1101, 1279), avg(1578, 1832), avg(1887, 2198)}}
+ -- Binding Heal
+ local BindingHeal = GetSpellInfo(32546)
+ spellData[BindingHeal] = {coeff = 1.5 / 3.5, levels = {64, 72, 78}, averages = {avg(1042, 1338), avg(1619, 2081), avg(1952, 2508)}, increase = {30, 24, 7}}
+ -- Penance
+ local Penance = GetSpellInfo(53007)
+ spellData[Penance] = {coeff = 0.857, ticks = 3, levels = {60, 70, 75, 80}, averages = {avg(670, 756), avg(805, 909), avg(1278, 1442), avg(1484, 1676)}}
+ -- Heal
+ local Heal = GetSpellInfo(2054)
+ spellData[Heal] = {coeff = 3 / 3.5, levels = {16, 22, 28, 34}, averages = {avg(295, 341), avg(429, 491), avg(566, 642), avg(712, 804)}, increase = {153, 185, 208, 207}}
+ -- Lesser Heal
+ local LesserHeal = GetSpellInfo(2050)
+ spellData[LesserHeal] = {levels = {1, 4, 10}, averages = {avg(46, 56), avg(71, 85), avg(135, 157)}, increase = {71, 83, 112}}
+
+ -- Talent data
+ local Grace = GetSpellInfo(47517)
+ -- Spiritual Healing (Add)
+ local SpiritualHealing = GetSpellInfo(14898)
+ talentData[SpiritualHealing] = {mod = 0.02, current = 0}
+ -- Empowered Healing (Add, also 0.04 for FH/BH)
+ local EmpoweredHealing = GetSpellInfo(33158)
+ talentData[EmpoweredHealing] = {mod = 0.08, current = 0}
+ -- Blessed Resilience (Add)
+ local BlessedResilience = GetSpellInfo(33142)
+ talentData[BlessedResilience] = {mod = 0.01, current = 0}
+ -- Focused Power (Add)
+ local FocusedPower = GetSpellInfo(33190)
+ talentData[FocusedPower] = {mod = 0.02, current = 0}
+ -- Divine Providence (Add)
+ local DivineProvidence = GetSpellInfo(47567)
+ talentData[DivineProvidence] = {mod = 0.02, current = 0}
+ -- Improved Renew (Add)
+ local ImprovedRenew = GetSpellInfo(14908)
+ talentData[ImprovedRenew] = {mod = 0.05, current = 0}
+ -- Empowered Renew (Multi, spell power)
+ local EmpoweredRenew = GetSpellInfo(63534)
+ talentData[EmpoweredRenew] = {mod = 0.05, current = 0}
+ -- Twin Disciplines (Add)
+ local TwinDisciplines = GetSpellInfo(47586)
+ talentData[TwinDisciplines] = {mod = 0.01, current = 0}
+
+ -- Keep track of who has grace on them
+ local activeGraceGUID, activeGraceModifier
+ AuraHandler = function(unit, guid)
+ local stack, _, _, _, caster = select(4, UnitBuff(unit, Grace))
+ if( caster == "player" ) then
+ activeGraceModifier = stack * 0.03
+ activeGraceGUID = guid
+ elseif( activeGraceGUID == guid ) then
+ activeGraceGUID = nil
+ end
+ end
+
+ -- Check for beacon when figuring out who to heal
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ if( spellName == BindingHeal ) then
+ return format("%s,%s", compressGUID[guid], compressGUID[playerGUID]), healAmount
+ elseif( spellName == PrayerofHealing ) then
+ local targets = compressGUID[guid]
+ local group = guidToGroup[guid]
+
+ for groupGUID, id in pairs(guidToGroup) do
+ local unit = guidToUnit[groupGUID]
+ if( id == group and guid ~= groupGUID and UnitIsVisible(unit) and not UnitHasVehicleUI(unit) ) then
+ targets = targets .. "," .. compressGUID[groupGUID]
+ end
+ end
+
+ return targets, healAmount
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ CalculateHotHealing = function(guid, spellID)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ local rank = HealComm.rankNumbers[spellRank]
+ local healAmount = hotData[spellName].averages[rank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local totalTicks
+
+ -- Add grace if it's active on them
+ if( activeGraceGUID == guid and activeGraceModifier ) then
+ healModifier = healModifier + activeGraceModifier
+ end
+
+ healModifier = healModifier + talentData[FocusedPower].current
+ healModifier = healModifier + talentData[BlessedResilience].current
+ healModifier = healModifier + talentData[SpiritualHealing].current
+
+ if( spellName == Renew ) then
+ healModifier = healModifier + talentData[ImprovedRenew].current
+ healModifier = healModifier + talentData[TwinDisciplines].current
+
+ -- Glyph of Renew, one less tick for +25% healing per tick. As this heals the same just faster, it has to be a flat 25% modifier
+ if( glyphCache[55674] ) then
+ healModifier = healModifier + 0.25
+ totalTicks = 4
+ else
+ totalTicks = 5
+ end
+
+ spellPower = spellPower * ((hotData[spellName].coeff * 1.88) * (1 + (talentData[EmpoweredRenew].current)))
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ end
+
+ healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+ return HOT_HEALS, ceil(healAmount), totalTicks, hotData[spellName].interval
+ end
+
+ -- If only every other class was as easy as Paladins
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local rank = HealComm.rankNumbers[spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+
+ -- Add grace if it's active on them
+ if( activeGraceGUID == guid ) then
+ healModifier = healModifier + activeGraceModifier
+ end
+
+ healModifier = healModifier + talentData[FocusedPower].current
+ healModifier = healModifier + talentData[BlessedResilience].current
+ healModifier = healModifier + talentData[SpiritualHealing].current
+
+ -- Greater Heal
+ if( spellName == GreaterHeal ) then
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].current))
+ -- Flash Heal
+ elseif( spellName == FlashHeal ) then
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].spent * 0.04))
+ -- Binding Heal
+ elseif( spellName == BindingHeal ) then
+ healModifier = healModifier + talentData[DivineProvidence].current
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].spent * 0.04))
+ -- Penance
+ elseif( spellName == Penance ) then
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ spellPower = spellPower / spellData[spellName].ticks
+ -- Prayer of Heaing
+ elseif( spellName == PrayerofHealing ) then
+ healModifier = healModifier + talentData[DivineProvidence].current
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ -- Heal
+ elseif( spellName == Heal ) then
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ -- Lesser Heal
+ elseif( spellName == LesserHeal ) then
+ local castTime = rank > 3 and 2.5 or rank == 2 and 2 or 1.5
+ spellPower = spellPower * ((castTime / 3.5) * 1.88)
+ end
+
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- Player has over a 100% chance to crit with Holy spells
+ if( GetSpellCritChance(2) >= 100 ) then
+ healAmount = healAmount * 1.50
+ end
+
+ -- Penance ticks 3 times, the player will see all 3 ticks, everyone else should only see the last 2
+ if( spellName == Penance ) then
+ return CHANNEL_HEALS, ceil(healAmount), 2, 3
+ end
+
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+-- SHAMANS
+-- All spells accurate as of 3.2.2 (build 10392)
+if( playerClass == "SHAMAN" ) then
+ LoadClassData = function()
+ -- Hot data
+ -- Riptide
+ local Riptide = GetSpellInfo(61295)
+ hotData[Riptide] = {interval = 3, ticks = 5, coeff = 0.50, levels = {60, 70, 75, 80}, averages = {665, 885, 1435, 1670}}
+ -- Earthliving Weapon proc
+ local Earthliving = GetSpellInfo(52000)
+ hotData[Earthliving] = {interval = 3, ticks = 4, coeff = 0.80, levels = {30, 40, 50, 60, 70, 80}, averages = {116, 160, 220, 348, 456, 652}}
+
+ -- Spell data
+ -- Chain Heal
+ local ChainHeal = GetSpellInfo(1064)
+ spellData[ChainHeal] = {coeff = 2.5 / 3.5, levels = {40, 46, 54, 61, 68, 74, 80}, increase = {100, 95, 85, 72, 45, 22, 0},
+ averages = {avg(320, 368), avg(405, 465), avg(551, 629), avg(605, 691), avg(826, 942), avg(906, 1034), avg(1055, 1205)}}
+ -- Healing Wave
+ local HealingWave = GetSpellInfo(331)
+ spellData[HealingWave] = {levels = {1, 6, 12, 18, 24, 32, 40, 48, 56, 60, 63, 70, 75, 80},
+ averages = {avg(34, 44), avg(64, 78), avg(129, 155), avg(268, 316), avg(376, 440), avg(536, 622), avg(740, 854), avg(1017, 1167), avg(1367, 1561), avg(1620, 1850), avg(1725, 1969), avg(2134, 2438), avg(2624, 2996), avg(3034, 3466)},
+ increase = {55, 74, 102, 142, 151, 158, 156, 150, 132, 110, 107, 71, 40, 0}}
+ -- Lesser Healing Wave
+ local LesserHealingWave = GetSpellInfo(8004)
+ spellData[LesserHealingWave] = {coeff = 1.5 / 3.5, levels = {20, 28, 36, 44, 52, 60, 66, 72, 77}, increase = {102, 109, 110, 108, 100, 84, 58, 40, 18},
+ averages = {avg(162, 186), avg(247, 281), avg(337, 381), avg(458, 514), avg(631, 705), avg(832, 928), avg(1039, 1185), avg(1382, 1578), avg(1606, 1834)}}
+
+ -- Talent data
+ local EarthShield = GetSpellInfo(49284)
+ -- Improved Chain Heal (Multi)
+ local ImpChainHeal = GetSpellInfo(30872)
+ talentData[ImpChainHeal] = {mod = 0.10, current = 0}
+ -- Tidal Waves (Add, this is a buff)
+ local TidalWaves = GetSpellInfo(51566)
+ talentData[TidalWaves] = {mod = 0.04, current = 0}
+ -- Healing Way (Multi, this goes from 8 -> 16 -> 25 so have to manually do the conversion)
+ local HealingWay = GetSpellInfo(29206)
+ talentData[HealingWay] = {mod = 0, current = 0}
+ -- Purification (Add)
+ local Purification = GetSpellInfo(16178)
+ talentData[Purification] = {mod = 0.02, current = 0}
+
+ -- Set bonuses
+ -- T7 Resto 4 piece, +5% healing on Chain Heal and Healing Wave
+ itemSetsData["T7 Resto"] = {40508, 40509, 40510, 40512, 40513, 39583, 39588, 39589, 39590, 39591}
+ -- T9 Resto 2 piece, +20% healing to Riptide
+ itemSetsData["T9 Resto"] = {48280, 48281, 48282, 48283, 48284, 48295, 48296, 48297, 48298, 48299, 48301, 48302, 48303, 48304, 48300, 48306, 48307, 48308, 48309, 48305, 48286, 48287, 48288, 48289, 48285, 48293, 48292, 48291, 48290, 48294}
+
+ -- Totems
+ local lhwTotems = {[42598] = 320, [42597] = 267, [42596] = 236, [42595] = 204, [25645] = 79, [22396] = 80, [23200] = 53}
+ local chTotems = {[45114] = 243, [38368] = 102, [28523] = 87}
+
+ -- Keep track of who has riptide on them
+ local riptideData, earthshieldList = {}, {}
+ AuraHandler = function(unit, guid)
+ riptideData[guid] = unitHasAura(unit, Riptide) and true or nil
+
+ -- Currently, Glyph of Lesser Healing Wave + Any Earth Shield increase the healing not just the players own
+ if( UnitBuff(unit, EarthShield) ) then
+ earthshieldList[guid] = true
+ elseif( earthshieldList[guid] ) then
+ earthshieldList[guid] = nil
+ end
+ end
+
+ -- Cast was interrupted, recheck if we still have the auras up
+ ResetChargeData = function(guid)
+ riptideData[guid] = guidToUnit[guid] and unitHasAura(guidToUnit[guid], Riptide) and true or nil
+ end
+
+ -- Lets a specific override on how many people this will hit
+ GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
+ -- Glyph of Healing Wave, heals you for 20% of your heal when you heal someone else
+ if( glyphCache[55440] and guid ~= playerGUID and spellName == HealingWave ) then
+ return format("%s,%d,%s,%d", compressGUID[guid], healAmount, compressGUID[playerGUID], healAmount * 0.20), -1
+ elseif( hasVariableTicks ) then
+ healAmount = tconcat(healAmount, "@")
+ end
+
+ return compressGUID[guid], healAmount
+ end
+
+ CalculateHotHealing = function(guid, spellID)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ local rank = HealComm.rankNumbers[spellRank]
+ local healAmount = hotData[spellName].averages[rank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+ local totalTicks
+
+ healModifier = healModifier + talentData[Purification].current
+
+ -- Riptide
+ if( spellName == Riptide ) then
+ if( equippedSetCache["T9 Resto"] >= 2 ) then
+ spModifier = spModifier * 1.20
+ end
+
+ spellPower = spellPower * (hotData[spellName].coeff * 1.88)
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ totalTicks = hotData[spellName].ticks
+ -- Glyph of Riptide, +6 seconds
+ if( glyphCache[63273] ) then totalTicks = totalTicks + 2 end
+
+ -- Earthliving Weapon
+ elseif( spellName == Earthliving ) then
+ spellPower = (spellPower * (hotData[spellName].coeff * 1.88) * 0.45)
+ spellPower = spellPower / hotData[spellName].ticks
+ healAmount = healAmount / hotData[spellName].ticks
+
+ totalTicks = hotData[spellName].ticks
+ end
+
+ healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+ return HOT_HEALS, healAmount, totalTicks, hotData[spellName].interval
+ end
+
+
+ -- If only every other class was as easy as Paladins
+ CalculateHealing = function(guid, spellName, spellRank)
+ local healAmount = averageHeal[spellName][spellRank]
+ local rank = HealComm.rankNumbers[spellRank]
+ local spellPower = GetSpellBonusHealing()
+ local healModifier, spModifier = playerHealModifier, 1
+
+ healModifier = healModifier + talentData[Purification].current
+
+ -- Chain Heal
+ if( spellName == ChainHeal ) then
+ healModifier = healModifier * (1 + talentData[ImpChainHeal].current)
+ healAmount = healAmount + (playerCurrentRelic and chTotems[playerCurrentRelic] or 0)
+
+ if( equippedSetCache["T7 Resto"] >= 4 ) then
+ healModifier = healModifier * 1.05
+ end
+
+ -- Add +25% from Riptide being up and reset the flag
+ if( riptideData[guid] ) then
+ healModifier = healModifier * 1.25
+ riptideData[guid] = nil
+ end
+
+ spellPower = spellPower * (spellData[spellName].coeff * 1.88)
+ -- Heaing Wave
+ elseif( spellName == HealingWave ) then
+ healModifier = healModifier * (talentData[HealingWay].spent == 3 and 1.25 or talentData[HealingWay].spent == 2 and 1.16 or talentData[HealingWay].spent == 1 and 1.08 or 1)
+
+ if( equippedSetCache["T7 Resto"] >= 4 ) then
+ healModifier = healModifier * 1.05
+ end
+
+ -- Totem of Spontaneous Regrowth, +88 Spell Power to Healing Wave
+ if( playerCurrentRelic == 27544 ) then
+ spellPower = spellPower + 88
+ end
+
+ local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
+ spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[TidalWaves].current)
+
+ -- Lesser Healing Wave
+ elseif( spellName == LesserHealingWave ) then
+ -- Glyph of Lesser Healing Wave, +20% healing on LHW if target has ES up
+ if( glyphCache[55438] and earthshieldList[guid] ) then
+ healModifier = healModifier * 1.20
+ end
+
+ spellPower = spellPower + (playerCurrentRelic and lhwTotems[playerCurrentRelic] or 0)
+ spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[TidalWaves].spent * 0.02)
+ end
+
+ healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
+
+ -- Player has over a 100% chance to crit with Nature spells
+ if( GetSpellCritChance(4) >= 100 ) then
+ healAmount = healAmount * 1.50
+ end
+
+ -- Apply the final modifier of any MS or self heal increasing effects
+ return DIRECT_HEALS, ceil(healAmount)
+ end
+ end
+end
+
+local function getName(spellID)
+ local name = GetSpellInfo(spellID)
+ --[===[@debug@
+ if( not name ) then
+ print(format("%s-r%s: Failed to find spellID %d", major, minor, spellID))
+ end
+ --@end-debug@]===]
+ return name or ""
+end
+
+-- Healing modifiers
+if( not HealComm.aurasUpdated ) then
+ HealComm.aurasUpdated = true
+ HealComm.selfModifiers = nil
+ HealComm.healingModifiers = nil
+end
+
+HealComm.currentModifiers = HealComm.currentModifiers or {}
+
+HealComm.selfModifiers = HealComm.selfModifiers or {
+ [64850] = 0.50, -- Unrelenting Assault
+ [65925] = 0.50, -- Unrelenting Assault
+ [54428] = 0.50, -- Divine Plea
+ [32346] = 0.50, -- Stolen Soul
+ [64849] = 0.75, -- Unrelenting Assault
+ [72221] = 1.05, -- Luck of the Draw
+ [70873] = 1.10, -- Emerald Vigor (Valithria Dreamwalker)
+ [31884] = 1.20, -- Avenging Wrath
+ [72393] = 0.25, -- Hopelessness
+ [72397] = 0.40, -- Hopelessness
+ [72391] = 0.50, -- Hopelessness
+ [72396] = 0.60, -- Hopelessness
+ [72390] = 0.75, -- Hopelessness
+ [72395] = 0.80, -- Hopelessness
+}
+
+-- The only spell in the game with a name conflict is Ray of Pain from the Nagrand Void Walkers
+HealComm.healingModifiers = HealComm.healingModifiers or {
+ [getName(30843)] = 0.00, -- Enfeeble
+ [getName(41292)] = 0.00, -- Aura of Suffering
+ [59513] = 0.00, -- Embrace of the Vampyr
+ [getName(55593)] = 0.00, -- Necrotic Aura
+ [28776] = 0.10, -- Necrotic Poison
+ [getName(34625)] = 0.25, -- Demolish
+ [getName(19716)] = 0.25, -- Gehennas' Curse
+ [getName(24674)] = 0.25, -- Veil of Shadow
+ [69633] = 0.25, -- Veil of Shadow, in German this is translated differently from the one above
+ [46296] = 0.25, -- Necrotic Poison
+ [54121] = 0.25, -- Necrotic Poison
+ -- Wound Poison still uses a unique spellID/spellName despite the fact that it's a static 50% reduction.
+ [getName(13218)] = 0.50, -- 1
+ [getName(13222)] = 0.50, -- 2
+ [getName(13223)] = 0.50, -- 3
+ [getName(13224)] = 0.50, -- 4
+ [getName(27189)] = 0.50, -- 5
+ [getName(57974)] = 0.50, -- 6
+ [getName(57975)] = 0.50, -- 7
+ [getName(20900)] = 0.50, -- Aimed Shot
+ [getName(44534)] = 0.50, -- Wretched Strike
+ [getName(21551)] = 0.50, -- Mortal Strike
+ [getName(40599)] = 0.50, -- Arcing Smash
+ [getName(36917)] = 0.50, -- Magma-Throwser's Curse
+ [getName(23169)] = 0.50, -- Brood Affliction: Green
+ [getName(22859)] = 0.50, -- Mortal Cleave
+ [getName(36023)] = 0.50, -- Deathblow
+ [getName(13583)] = 0.50, -- Curse of the Deadwood
+ [getName(32378)] = 0.50, -- Filet
+ [getName(35189)] = 0.50, -- Solar Strike
+ [getName(32315)] = 0.50, -- Soul Strike
+ [getName(60084)] = 0.50, -- The Veil of Shadow
+ [getName(45885)] = 0.50, -- Shadow Spike
+ [getName(69674)] = 0.50, -- Mutated Infection (Rotface)
+ [36693] = 0.55, -- Necrotic Poison
+ [getName(63038)] = 0.75, -- Dark Volley
+ [getName(52771)] = 0.75, -- Wounding Strike
+ [getName(48291)] = 0.75, -- Fetid Healing
+ [getName(34366)] = 0.75, -- Ebon Poison
+ [getName(54525)] = 0.80, -- Shroud of Darkness (This might be wrong)
+ [getName(48301)] = 0.80, -- Mind Trauma (Improved Mind Blast)
+ [getName(68391)] = 0.80, -- Permafrost, the debuff is generic no way of seeing 7/13/20, go with 20
+ [getName(52645)] = 0.80, -- Hex of Weakness
+ [getName(34073)] = 0.85, -- Curse of the Bleeding Hollow
+ [getName(43410)] = 0.90, -- Chop
+ [getName(70588)] = 0.90, -- Suppression (Valithria Dreamwalker NPCs?)
+ [getName(34123)] = 1.06, -- Tree of Life
+ [getName(64844)] = 1.10, -- Divine Hymn
+ [getName(47788)] = 1.40, -- Guardian Spirit
+ [getName(38387)] = 1.50, -- Bane of Infinity
+ [getName(31977)] = 1.50, -- Curse of Infinity
+ [getName(41350)] = 2.00, -- Aura of Desire
+ [73762] = 1.05, -- Strength of Wrynn (5%)
+ [73816] = 1.05, -- Hellscream's Warsong (5%)
+ [73824] = 1.10, -- Strength of Wrynn (10%)
+ [73818] = 1.10, -- Hellscream's Warsong (10%)
+ [73825] = 1.15, -- Strength of Wrynn (15%)
+ [73819] = 1.15, -- Hellscream's Warsong (15%)
+ [73826] = 1.20, -- Strength of Wrynn (20%)
+ [73820] = 1.20, -- Hellscream's Warsong (20%)
+ [73827] = 1.25, -- Strength of Wrynn (25%)
+ [73821] = 1.25, -- Hellscream's Warsong (25%)
+ [73828] = 1.30, -- Strength of Wrynn (30%)
+ [73822] = 1.30, -- Hellscream's Warsong (30%)
+}
+
+HealComm.healingStackMods = HealComm.healingStackMods or {
+ -- Enervating Band
+ [getName(74502)] = function(name, rank, icon, stacks) return 1 - stacks * 0.02 end,
+ -- Tenacity
+ [getName(58549)] = function(name, rank, icon, stacks) return icon == "Interface\\Icons\\Ability_Warrior_StrengthOfArms" and stacks ^ 1.18 or 1 end,
+ -- Focused Will
+ [getName(45242)] = function(name, rank, icon, stacks) return 1 + (stacks * (0.02 + rankNumbers[rank] / 100)) end,
+ -- Nether Portal - Dominance
+ [getName(30423)] = function(name, rank, icon, stacks) return 1 + stacks * 0.01 end,
+ -- Dark Touched
+ [getName(45347)] = function(name, rank, icon, stacks) return 1 - stacks * 0.04 end,
+ -- Necrotic Strike
+ [getName(60626)] = function(name, rank, icon, stacks) return 1 - stacks * 0.10 end,
+ -- Mortal Wound
+ [getName(28467)] = function(name, rank, icon, stacks) return 1 - stacks * 0.10 end,
+ -- Furious Strikes
+ [getName(56112)] = function(name, rank, icon, stacks) return 1 - stacks * 0.25 end,
+}
+
+local healingStackMods, selfModifiers = HealComm.healingStackMods, HealComm.selfModifiers
+local healingModifiers, currentModifiers = HealComm.healingModifiers, HealComm.currentModifiers
+
+local distribution
+local CTL = ChatThrottleLib
+local function sendMessage(msg)
+ if( distribution and len(msg) <= 240 ) then
+ CTL:SendAddonMessage("BULK", COMM_PREFIX, msg, distribution)
+ end
+end
+
+-- Keep track of where all the data should be going
+local instanceType
+local function updateDistributionChannel()
+ local lastChannel = distribution
+ if( instanceType == "pvp" or instanceType == "arena" ) then
+ distribution = "BATTLEGROUND"
+ elseif( GetNumRaidMembers() > 0 ) then
+ distribution = "RAID"
+ elseif( GetNumPartyMembers() > 0 ) then
+ distribution = "PARTY"
+ else
+ distribution = nil
+ end
+
+ if( distribution == lastChannel ) then return end
+
+ -- If the player is not a healer, some events can be disabled until the players grouped.
+ if( distribution ) then
+ HealComm.eventFrame:RegisterEvent("CHAT_MSG_ADDON")
+ if( not isHealerClass ) then
+ HealComm.eventFrame:RegisterEvent("UNIT_AURA")
+ HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED")
+ HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
+ HealComm.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ end
+ else
+ HealComm.eventFrame:UnregisterEvent("CHAT_MSG_ADDON")
+ if( not isHealerClass ) then
+ HealComm.eventFrame:UnregisterEvent("UNIT_AURA")
+ HealComm.eventFrame:UnregisterEvent("UNIT_SPELLCAST_DELAYED")
+ HealComm.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
+ HealComm.eventFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ end
+ end
+end
+
+-- Figure out where we should be sending messages and wipe some caches
+function HealComm:PLAYER_ENTERING_WORLD()
+ HealComm.eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
+ HealComm:ZONE_CHANGED_NEW_AREA()
+end
+
+function HealComm:ZONE_CHANGED_NEW_AREA()
+ local pvpType = GetZonePVPInfo()
+ local type = select(2, IsInInstance())
+
+ HealComm.zoneHealModifier = 1
+ if( pvpType == "combat" or type == "arena" or type == "pvp" ) then
+ HealComm.zoneHealModifier = 0.90
+ end
+
+ if( type ~= instanceType ) then
+ instanceType = type
+
+ updateDistributionChannel()
+ clearPendingHeals()
+ twipe(activeHots)
+ end
+
+ instanceType = type
+end
+
+local alreadyAdded = {}
+function HealComm:UNIT_AURA(unit)
+ if not unit then return end
+
+ local guid = UnitGUID(unit)
+ if( not guidToUnit[guid] ) then return end
+ local increase, decrease, playerIncrease, playerDecrease = 1, 1, 1, 1
+
+ -- Scan buffs
+ local id = 1
+ while( true ) do
+ local name, rank, icon, stack, _, _, _, _, _, _, spellID = UnitAura(unit, id, "HELPFUL")
+ if( not name ) then break end
+ -- Prevent buffs like Tree of Life that have the same name for the shapeshift/healing increase from being calculated twice
+ if( not alreadyAdded[name] ) then
+ alreadyAdded[name] = true
+
+ if( healingModifiers[spellID] ) then
+ increase = increase * healingModifiers[spellID]
+ elseif( healingModifiers[name] ) then
+ increase = increase * healingModifiers[name]
+ elseif( healingStackMods[name] ) then
+ increase = increase * healingStackMods[name](name, rank, icon, stack)
+ end
+
+ if( unit == "player" and selfModifiers[spellID] ) then
+ playerIncrease = playerIncrease * selfModifiers[spellID]
+ end
+ end
+
+ id = id + 1
+ end
+
+ -- Scan debuffs
+ id = 1
+ while( true ) do
+ local name, rank, icon, stack, _, _, _, _, _, _, spellID = UnitAura(unit, id, "HARMFUL")
+ if( not name ) then break end
+
+ if( healingModifiers[spellID] ) then
+ decrease = min(decrease, healingModifiers[spellID])
+ elseif( healingModifiers[name] ) then
+ decrease = min(decrease, healingModifiers[name])
+ elseif( healingStackMods[name] ) then
+ decrease = min(decrease, healingStackMods[name](name, rank, icon, stack))
+ end
+
+ if( unit == "player" and selfModifiers[spellID] ) then
+ playerDecrease = min(playerDecrease, selfModifiers[spellID])
+ end
+
+ id = id + 1
+ end
+
+ -- Check if modifier changed
+ local modifier = increase * decrease
+ if( modifier ~= currentModifiers[guid] ) then
+ if( currentModifiers[guid] or modifier ~= 1 ) then
+ currentModifiers[guid] = modifier
+ self.callbacks:Fire("HealCommArena_ModifierChanged", guid, modifier)
+ else
+ currentModifiers[guid] = modifier
+ end
+ end
+
+ twipe(alreadyAdded)
+
+ if( unit == "player" ) then
+ playerHealModifier = playerIncrease * playerDecrease
+ end
+
+ -- Class has a specific monitor it needs for auras
+ if( AuraHandler ) then
+ AuraHandler(unit, guid)
+ end
+end
+
+-- Monitor glyph changes
+function HealComm:GlyphsUpdated(id)
+ local spellID = glyphCache[id]
+
+ -- Invalidate the old cache value
+ if( spellID ) then
+ glyphCache[spellID] = nil
+ glyphCache[id] = nil
+ end
+
+ -- Cache the new one if any
+ local enabled, _, glyphID = GetGlyphSocketInfo(id)
+ if( enabled and glyphID ) then
+ glyphCache[glyphID] = true
+ glyphCache[id] = glyphID
+ end
+end
+
+HealComm.GLYPH_ADDED = HealComm.GlyphsUpdated
+HealComm.GLYPH_REMOVED = HealComm.GlyphsUpdated
+HealComm.GLYPH_UPDATED = HealComm.GlyphsUpdated
+
+-- Invalidate he average cache to recalculate for spells that increase in power due to leveling up (but not training new ranks)
+function HealComm:PLAYER_LEVEL_UP(level)
+ for spell, average in pairs(averageHeal) do
+ twipe(average)
+
+ average.spell = spell
+ end
+
+ -- WoWProgramming says this is a string, why this is a string I do not know.
+ playerLevel = tonumber(level) or UnitLevel("player")
+end
+
+-- Cache player talent data for spells we need
+function HealComm:PLAYER_TALENT_UPDATE()
+ for tabIndex=1, GetNumTalentTabs() do
+ for i=1, GetNumTalents(tabIndex) do
+ local name, _, _, _, spent = GetTalentInfo(tabIndex, i)
+ if( name and talentData[name] ) then
+ talentData[name].current = talentData[name].mod * spent
+ talentData[name].spent = spent
+ end
+ end
+ end
+end
+
+-- Save the currently equipped range weapon
+local RANGED_SLOT = GetInventorySlotInfo("RangedSlot")
+function HealComm:PLAYER_EQUIPMENT_CHANGED()
+ -- Caches set bonus info, as you can't reequip set bonus gear in combat no sense in checking it
+ if( not InCombatLockdown() ) then
+ for name, items in pairs(itemSetsData) do
+ equippedSetCache[name] = 0
+ for _, itemID in pairs(items) do
+ if( IsEquippedItem(itemID) ) then
+ equippedSetCache[name] = equippedSetCache[name] + 1
+ end
+ end
+ end
+ end
+
+ -- Check relic
+ local relic = GetInventoryItemLink("player", RANGED_SLOT)
+ playerCurrentRelic = relic and tonumber(match(relic, "item:(%d+):")) or nil
+end
+
+-- COMM CODE
+local function loadHealAmount(...)
+ local tbl = HealComm:RetrieveTable()
+ for i=1, select("#", ...) do
+ tbl[i] = tonumber((select(i, ...)))
+ end
+
+ return tbl
+end
+
+-- Direct heal started
+local function loadHealList(pending, amount, stack, endTime, ticksLeft, ...)
+ twipe(tempPlayerList)
+
+ -- For the sake of consistency, even a heal doesn't have multiple end times like a hot, it'll be treated as such in the DB
+ if( amount ~= -1 and amount ~= "-1" ) then
+ amount = not pending.hasVariableTicks and amount or loadHealAmount(split("@", amount))
+
+ for i=1, select("#", ...) do
+ local guid = select(i, ...)
+ if( guid ) then
+ updateRecord(pending, decompressGUID[guid], amount, stack, endTime, ticksLeft)
+ tinsert(tempPlayerList, decompressGUID[guid])
+ end
+ end
+ else
+ for i=1, select("#", ...), 2 do
+ local guid = select(i, ...)
+ local amount = not pending.hasVariableTicks and tonumber((select(i + 1, ...))) or loadHealAmount(split("@", amount))
+ if( guid and amount ) then
+ updateRecord(pending, decompressGUID[guid], amount, stack, endTime, ticksLeft)
+ tinsert(tempPlayerList, decompressGUID[guid])
+ end
+ end
+ end
+end
+
+local function parseDirectHeal(casterGUID, spellID, amount, ...)
+ local spellName = GetSpellInfo(spellID)
+ local unit = guidToUnit[casterGUID]
+ if( not unit or not spellName or not amount or select("#", ...) == 0 ) then return end
+
+ local endTime = select(6, UnitCastingInfo(unit))
+ if( not endTime ) then return end
+
+ pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
+ pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
+
+ local pending = pendingHeals[casterGUID][spellName]
+ twipe(pending)
+ pending.endTime = endTime / 1000
+ pending.spellID = spellID
+ pending.bitType = DIRECT_HEALS
+
+ loadHealList(pending, amount, 1, 0, nil, ...)
+
+ HealComm.callbacks:Fire("HealCommArena_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+end
+
+HealComm.parseDirectHeal = parseDirectHeal
+
+-- Channeled heal started
+local function parseChannelHeal(casterGUID, spellID, amount, totalTicks, ...)
+ local spellName = GetSpellInfo(spellID)
+ local unit = guidToUnit[casterGUID]
+ if( not unit or not spellName or not totalTicks or not amount or select("#", ...) == 0 ) then return end
+
+ local startTime, endTime = select(5, UnitChannelInfo(unit))
+ if( not startTime or not endTime ) then return end
+
+ pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
+ pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
+
+ local inc = amount == -1 and 2 or 1
+ local pending = pendingHeals[casterGUID][spellName]
+ twipe(pending)
+ pending.startTime = startTime / 1000
+ pending.endTime = endTime / 1000
+ pending.duration = max(pending.duration or 0, pending.endTime - pending.startTime)
+ pending.totalTicks = totalTicks
+ pending.tickInterval = (pending.endTime - pending.startTime) / totalTicks
+ pending.spellID = spellID
+ pending.isMultiTarget = (select("#", ...) / inc) > 1
+ pending.bitType = CHANNEL_HEALS
+
+ loadHealList(pending, amount, 1, 0, ceil(pending.duration / pending.tickInterval), ...)
+
+ HealComm.callbacks:Fire("HealCommArena_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+end
+
+-- Hot heal started
+-- When the person is within visible range of us, the aura is available by the time the message reaches the target
+-- as such, we can rely that at least one person is going to have the aura data on them (and that it won't be different, at least for this cast)
+local function findAura(casterGUID, spellName, spellRank, inc, ...)
+ for i=1, select("#", ...), inc do
+ local guid = decompressGUID[select(i, ...)]
+ local unit = guid and guidToUnit[guid]
+ if( unit and UnitIsVisible(unit) ) then
+ local id = 1
+ while( true ) do
+ local name, rank, _, stack, _, duration, endTime, caster = UnitBuff(unit, id)
+ if( not name ) then break end
+
+ if( name == spellName and spellRank == rank and caster and UnitGUID(caster) == casterGUID ) then
+ return (stack and stack > 0 and stack or 1), duration, endTime
+ end
+
+ id = id + 1
+ end
+ end
+ end
+end
+
+local function parseHotHeal(casterGUID, wasUpdated, spellID, tickAmount, totalTicks, tickInterval, ...)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ -- If the user is on 3.3, then anything without a total ticks attached to it is rejected
+ if( ( IS_BUILD30300 and not totalTicks ) or not tickAmount or not spellName or select("#", ...) == 0 ) then return end
+
+ -- Retrieve the hot information
+ local inc = ( tickAmount == -1 or tickAmount == "-1" ) and 2 or 1
+ local stack, duration, endTime = findAura(casterGUID, spellName, spellRank, inc, ...)
+ if( not stack or not duration or not endTime ) then return end
+
+ pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
+ pendingHeals[casterGUID][spellID] = pendingHeals[casterGUID][spellID] or {}
+
+ local pending = pendingHeals[casterGUID][spellID]
+ pending.duration = duration
+ pending.endTime = endTime
+ pending.stack = stack
+ pending.totalTicks = totalTicks or duration / tickInterval
+ pending.tickInterval = totalTicks and duration / totalTicks or tickInterval
+ pending.spellID = spellID
+ pending.hasVariableTicks = type(tickAmount) == "string"
+ pending.isMutliTarget = (select("#", ...) / inc) > 1
+ pending.bitType = HOT_HEALS
+
+ -- As you can't rely on a hot being the absolutely only one up, have to apply the total amount now :<
+ local ticksLeft = ceil((endTime - GetTime()) / pending.tickInterval)
+ loadHealList(pending, tickAmount, stack, endTime, ticksLeft, ...)
+
+ if( not wasUpdated ) then
+ HealComm.callbacks:Fire("HealCommArena_HealStarted", casterGUID, spellID, pending.bitType, endTime, unpack(tempPlayerList))
+ else
+ HealComm.callbacks:Fire("HealCommArena_HealUpdated", casterGUID, spellID, pending.bitType, endTime, unpack(tempPlayerList))
+ end
+end
+
+local function parseHotBomb(casterGUID, wasUpdated, spellID, amount, ...)
+ local spellName, spellRank = GetSpellInfo(spellID)
+ if( not amount or not spellName or select("#", ...) == 0 ) then return end
+
+ -- If we don't have a pending hot then there is no bomb as far as were concerned
+ local hotPending = pendingHeals[casterGUID] and pendingHeals[casterGUID][spellID]
+ if( not hotPending or not hotPending.bitType ) then return end
+ hotPending.hasBomb = true
+
+ pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
+
+ local pending = pendingHeals[casterGUID][spellName]
+ pending.endTime = hotPending.endTime
+ pending.spellID = spellID
+ pending.bitType = BOMB_HEALS
+ pending.stack = hotPending.stack
+
+ loadHealList(pending, amount, pending.stack, pending.endTime, nil, ...)
+
+ if( not wasUpdated ) then
+ HealComm.callbacks:Fire("HealCommArena_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+ else
+ HealComm.callbacks:Fire("HealCommArena_HealUpdated", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+ end
+end
+
+-- Heal finished
+local function parseHealEnd(casterGUID, pending, checkField, spellID, interrupted, ...)
+ local spellName = GetSpellInfo(spellID)
+ if( not spellName or not pendingHeals[casterGUID] ) then return end
+
+ -- Hots use spell IDs while everything else uses spell names. Avoids naming conflicts for multi-purpose spells such as Lifebloom or Regrowth
+ if( not pending ) then
+ pending = checkField == "id" and pendingHeals[casterGUID][spellID] or pendingHeals[casterGUID][spellName]
+ end
+ if( not pending or not pending.bitType ) then return end
+
+ twipe(tempPlayerList)
+
+ if( select("#", ...) == 0 ) then
+ for i=#(pending), 1, -5 do
+ tinsert(tempPlayerList, pending[i - 4])
+ removeRecord(pending, pending[i - 4])
+ end
+ else
+ for i=1, select("#", ...) do
+ local guid = decompressGUID[select(i, ...)]
+
+ tinsert(tempPlayerList, guid)
+ removeRecord(pending, guid)
+ end
+ end
+
+ -- Double check and make sure we actually removed at least one person
+ if( #(tempPlayerList) == 0 ) then return end
+
+ -- Heals that also have a bomb associated to them have to end at this point, they will fire there own callback too
+ local bombPending = pending.hasBomb and pendingHeals[casterGUID][spellName]
+ if( bombPending and bombPending.bitType ) then
+ parseHealEnd(casterGUID, bombPending, "name", spellID, interrupted, ...)
+ end
+
+ local bitType = pending.bitType
+ -- Clear data if we're done
+ if( #(pending) == 0 ) then twipe(pending) end
+
+ HealComm.callbacks:Fire("HealCommArena_HealStopped", casterGUID, spellID, bitType, interrupted, unpack(tempPlayerList))
+end
+
+HealComm.parseHealEnd = parseHealEnd
+
+-- Heal delayed
+local function parseHealDelayed(casterGUID, startTime, endTime, spellName)
+ local pending = pendingHeals[casterGUID][spellName]
+ -- It's possible to get duplicate interrupted due to raid1 = party1, player = raid# etc etc, just block it here
+ if( pending.endTime == endTime and pending.startTime == startTime ) then return end
+
+ -- Casted heal
+ if( pending.bitType == DIRECT_HEALS ) then
+ pending.startTime = startTime
+ pending.endTime = endTime
+ -- Channel heal
+ elseif( pending.bitType == CHANNEL_HEALS ) then
+ pending.startTime = startTime
+ pending.endTime = endTime
+ pending.tickInterval = (pending.endTime - pending.startTime)
+ else
+ return
+ end
+
+ twipe(tempPlayerList)
+ for i=1, #(pending), 5 do
+ tinsert(tempPlayerList, pending[i])
+ end
+
+ HealComm.callbacks:Fire("HealCommArena_HealDelayed", casterGUID, pending.spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
+end
+
+-- After checking around 150-200 messages in battlegrounds, server seems to always be passed (if they are from another server)
+-- Channels use tick total because the tick interval varies by haste
+-- Hots use tick interval because the total duration varies but the tick interval stays the same
+function HealComm:CHAT_MSG_ADDON(prefix, message, channel, sender)
+ if( prefix ~= COMM_PREFIX or channel ~= distribution or sender == playerName ) then return end
+
+ local commType, extraArg, spellID, arg1, arg2, arg3, arg4, arg5, arg6 = split(":", message)
+ local casterGUID = UnitGUID(sender)
+ spellID = tonumber(spellID)
+
+ if( not commType or not spellID or not casterGUID ) then return end
+
+ -- New direct heal - D::::target1,target2...
+ if( commType == "D" and arg1 and arg2 ) then
+ parseDirectHeal(casterGUID, spellID, tonumber(arg1), split(",", arg2))
+ -- New channel heal - C:::::target1,target2...
+ elseif( commType == "C" and arg1 and arg3 ) then
+ parseChannelHeal(casterGUID, spellID, tonumber(arg1), tonumber(arg2), split(",", arg3))
+ -- New hot with a "bomb" component - B::::target1,target2::::target1,target2...
+ elseif( commType == "B" and arg1 and arg6 ) then
+ parseHotHeal(casterGUID, false, spellID, tonumber(arg3), tonumber(extraArg), tonumber(arg5), split(",", arg6))
+ parseHotBomb(casterGUID, false, spellID, tonumber(arg1), split(",", arg2))
+ -- New hot - H::::::target1,target2...
+ elseif( commType == "H" and arg1 and arg4 ) then
+ parseHotHeal(casterGUID, false, spellID, tonumber(arg1), tonumber(extraArg), tonumber(arg3), split(",", arg4))
+ -- New updated heal somehow before ending - U:::::target1,target2...
+ elseif( commType == "U" and arg1 and arg3 ) then
+ parseHotHeal(casterGUID, true, spellID, tonumber(arg1), tonumber(extraArg), tonumber(arg2), split(",", arg3))
+ -- New variable tick hot - VH::::::target1,target2...
+ elseif( commType == "VH" and arg1 and arg4 ) then
+ parseHotHeal(casterGUID, false, spellID, arg1, tonumber(arg3), nil, split(",", arg4))
+ -- New updated variable tick hot - U:::amount1@amount2@amount3::target1,target2...
+ elseif( commType == "VU" and arg1 and arg3 ) then
+ parseHotHeal(casterGUID, true, spellID, arg1, tonumber(arg2), nil, split(",", arg3))
+ -- New updated bomb hot - UB::::target1,target2:::target1,target2...
+ elseif( commType == "UB" and arg1 and arg5 ) then
+ parseHotHeal(casterGUID, true, spellID, tonumber(arg3), tonumber(extraArg), tonumber(arg4), split(",", arg5))
+ parseHotBomb(casterGUID, true, spellID, tonumber(arg1), split(",", arg2))
+ -- Heal stopped - S::::target1,target2...
+ elseif( commType == "S" or commType == "HS" ) then
+ local interrupted = arg1 == "1" and true or false
+ local type = commType == "HS" and "id" or "name"
+
+ if( arg2 and arg2 ~= "" ) then
+ parseHealEnd(casterGUID, nil, type, spellID, interrupted, split(",", arg2))
+ else
+ parseHealEnd(casterGUID, nil, type, spellID, interrupted)
+ end
+ end
+end
+
+-- Bucketing reduces the number of events triggered for heals such as Tranquility that hit multiple targets
+-- instead of firing 5 events * ticks it will fire 1 (maybe 2 depending on lag) events
+HealComm.bucketHeals = HealComm.bucketHeals or {}
+local bucketHeals = HealComm.bucketHeals
+local BUCKET_FILLED = 0.30
+
+HealComm.bucketFrame = HealComm.bucketFrame or CreateFrame("Frame")
+HealComm.bucketFrame:Hide()
+
+HealComm.bucketFrame:SetScript("OnUpdate", function(self, elapsed)
+ local totalLeft = 0
+ for casterGUID, spells in pairs(bucketHeals) do
+ for id, data in pairs(spells) do
+ if( data.timeout ) then
+ data.timeout = data.timeout - elapsed
+
+ if( data.timeout <= 0 ) then
+ -- This shouldn't happen, on the offhand chance it does then don't bother sending an event
+ if( #(data) == 0 or not data.spellID or not data.spellName ) then
+ twipe(data)
+ -- We're doing a bucket for a tick heal like Tranquility or Wild Growth
+ elseif( data.type == "tick" ) then
+ local pending = pendingHeals[casterGUID] and ( pendingHeals[casterGUID][data.spellID] or pendingHeals[casterGUID][data.spellName] )
+ if( pending and pending.bitType ) then
+ local endTime = select(3, getRecord(pending, data[1]))
+ HealComm.callbacks:Fire("HealCommArena_HealUpdated", casterGUID, pending.spellID, pending.bitType, endTime, unpack(data))
+ end
+
+ twipe(data)
+ -- We're doing a bucket for a cast thats a multi-target heal like Wild Growth or Prayer of Healing
+ elseif( data.type == "heal" ) then
+ local type, amount, totalTicks, tickInterval, _, hasVariableTicks = CalculateHotHealing(data[1], data.spellID)
+ if( type ) then
+ local targets, amount = GetHealTargets(type, data[1], hasVariableTicks and amount or max(amount, 0), data.spellName, data, hasVariableTicks)
+ parseHotHeal(playerGUID, false, data.spellID, amount, totalTicks, tickInterval, split(",", targets))
+
+ if( not hasVariableTicks ) then
+ sendMessage(format("H:%d:%d:%d::%d:%s", totalTicks, data.spellID, amount, tickInterval, targets))
+ else
+ sendMessage(format("VH::%d:%s::%d:%s", data.spellID, amount, totalTicks, targets))
+ end
+ end
+
+ twipe(data)
+ end
+ else
+ totalLeft = totalLeft + 1
+ end
+ end
+ end
+ end
+
+ if( totalLeft <= 0 ) then
+ self:Hide()
+ end
+end)
+
+-- Monitor aura changes as well as new hots being cast
+local eventRegistered = {["SPELL_HEAL"] = true, ["SPELL_PERIODIC_HEAL"] = true}
+if( isHealerClass ) then
+ eventRegistered["SPELL_AURA_REMOVED"] = true
+ eventRegistered["SPELL_AURA_APPLIED"] = true
+ eventRegistered["SPELL_AURA_REFRESH"] = true
+ eventRegistered["SPELL_AURA_APPLIED_DOSE"] = true
+ eventRegistered["SPELL_AURA_REMOVED_DOSE"] = true
+end
+
+local COMBATLOG_OBJECT_AFFILIATION_MINE = COMBATLOG_OBJECT_AFFILIATION_MINE
+local CAN_HEAL = bor(COMBATLOG_OBJECT_REACTION_FRIENDLY, COMBATLOG_OBJECT_REACTION_NEUTRAL)
+function HealComm:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
+ if( not eventRegistered[eventType] ) then return end
+
+ -- Heal or hot ticked that the library is tracking
+ -- It's more efficient/accurate to have the library keep track of this locally, spamming the comm channel would not be a very good thing especially when a single player can have 4 - 8 hots/channels going on them.
+ if( eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" ) then
+ local spellID, spellName, spellSchool = ...
+ local pending = sourceGUID and pendingHeals[sourceGUID] and (pendingHeals[sourceGUID][spellID] or pendingHeals[sourceGUID][spellName])
+ if( pending and pending[destGUID] and pending.bitType and band(pending.bitType, OVERTIME_HEALS) > 0 ) then
+ local amount, stack, endTime, ticksLeft = getRecord(pending, destGUID)
+ ticksLeft = ticksLeft - 1
+ endTime = GetTime() + pending.tickInterval * ticksLeft
+ if( pending.hasVariableTicks ) then tremove(amount, 1) end
+
+ updateRecord(pending, destGUID, amount, stack, endTime, ticksLeft)
+
+ if( pending.isMultiTarget ) then
+ bucketHeals[sourceGUID] = bucketHeals[sourceGUID] or {}
+ bucketHeals[sourceGUID][spellID] = bucketHeals[sourceGUID][spellID] or {}
+
+ local spellBucket = bucketHeals[sourceGUID][spellID]
+ if( not spellBucket[destGUID] ) then
+ spellBucket.timeout = BUCKET_FILLED
+ spellBucket.type = "tick"
+ spellBucket.spellName = spellName
+ spellBucket.spellID = spellID
+ spellBucket[destGUID] = true
+ tinsert(spellBucket, destGUID)
+
+ self.bucketFrame:Show()
+ end
+ else
+ HealComm.callbacks:Fire("HealCommArena_HealUpdated", sourceGUID, spellID, pending.bitType, endTime, destGUID)
+ end
+ end
+
+ -- New hot was applied
+ elseif( ( eventType == "SPELL_AURA_APPLIED" or eventType == "SPELL_AURA_REFRESH" or eventType == "SPELL_AURA_APPLIED_DOSE" ) and band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
+ local spellID, spellName, spellSchool, auraType = ...
+ if( hotData[spellName] ) then
+ -- Multi target heal so put it in the bucket
+ if( hotData[spellName].isMulti ) then
+ bucketHeals[sourceGUID] = bucketHeals[sourceGUID] or {}
+ bucketHeals[sourceGUID][spellName] = bucketHeals[sourceGUID][spellName] or {}
+
+ -- For some reason, Glyph of Prayer of Healing fires a SPELL_AURA_APPLIED then a SPELL_AURA_REFRESH right after
+ local spellBucket = bucketHeals[sourceGUID][spellName]
+ if( not spellBucket[destGUID] ) then
+ spellBucket.timeout = BUCKET_FILLED
+ spellBucket.type = "heal"
+ spellBucket.spellName = spellName
+ spellBucket.spellID = spellID
+ spellBucket[destGUID] = true
+ tinsert(spellBucket, destGUID)
+
+ self.bucketFrame:Show()
+ end
+ return
+ end
+
+ -- Single target so we can just send it off now thankfully
+ local type, amount, totalTicks, tickInterval, bombAmount, hasVariableTicks = CalculateHotHealing(destGUID, spellID)
+ if( type ) then
+ local targets, amount = GetHealTargets(type, destGUID, hasVariableTicks and amount or max(amount, 0), spellName, hasVariableTicks)
+ parseHotHeal(sourceGUID, false, spellID, amount, totalTicks, tickInterval, split(",", targets))
+
+ -- Hot with a bomb!
+ if( bombAmount ) then
+ local bombTargets, bombAmount = GetHealTargets(BOMB_HEALS, destGUID, max(bombAmount, 0), spellName)
+ parseHotBomb(sourceGUID, false, spellID, bombAmount, split(",", bombTargets))
+ sendMessage(format("B:%d:%d:%d:%s:%d::%d:%s", totalTicks, spellID, bombAmount, bombTargets, amount, tickInterval, targets))
+ elseif( hasVariableTicks ) then
+ sendMessage(format("VH::%d:%s::%d:%s", spellID, amount, totalTicks, targets))
+ -- Normal hot, nothing fancy
+ else
+ sendMessage(format("H:%d:%d:%d::%d:%s", totalTicks, spellID, amount, tickInterval, targets))
+ end
+ end
+ end
+ -- Single stack of a hot was removed, this only applies when going from 2 -> 1, when it goes from 1 -> 0 it fires SPELL_AURA_REMOVED
+ elseif( eventType == "SPELL_AURA_REMOVED_DOSE" and band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
+ local spellID, spellName, spellSchool, auraType, stacks = ...
+ local pending = sourceGUID and pendingHeals[sourceGUID] and pendingHeals[sourceGUID][spellID]
+ if( pending and pending.bitType ) then
+ local amount = getRecord(pending, destGUID)
+ if( amount ) then
+ parseHotHeal(sourceGUID, true, spellID, amount, pending.totalTicks, pending.tickInterval, compressGUID[destGUID])
+
+ -- Plant the bomb
+ local bombPending = pending.hasBomb and pendingHeals[sourceGUID][spellName]
+ if( bombPending and bombPending.bitType ) then
+ local bombAmount = getRecord(bombPending, destGUID)
+ if( bombAmount ) then
+ parseHotBomb(sourceGUID, true, spellID, bombAmount, compressGUID[destGUID])
+
+ sendMessage(format("UB:%s:%d:%d:%s:%d:%d:%s", pending.totalTicks, spellID, bombAmount, compressGUID[destGUID], amount, pending.tickInterval, compressGUID[destGUID]))
+ return
+ end
+ end
+
+ -- Failed to find any sort of bomb-y info we needed or it doesn't have a bomb anyway
+ if( pending.hasVariableTicks ) then
+ sendMessage(format("VU::%d:%s:%d:%s", spellID, amount, pending.totalTicks, compressGUID[destGUID]))
+ else
+ sendMessage(format("U:%s:%d:%d:%d:%s", spellID, amount, pending.totalTicks, pending.tickInterval, compressGUID[destGUID]))
+ end
+ end
+ end
+
+ -- Aura faded
+ elseif( eventType == "SPELL_AURA_REMOVED" and band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
+ local spellID, spellName, spellSchool, auraType = ...
+
+ -- Hot faded that we cast
+ if( hotData[spellName] ) then
+ parseHealEnd(sourceGUID, nil, "id", spellID, false, compressGUID[destGUID])
+ sendMessage(format("HS::%d::%s", spellID, compressGUID[destGUID]))
+ end
+ end
+end
+
+-- Spell cast magic
+-- When auto self cast is on, the UNIT_SPELLCAST_SENT event will always come first followed by the funciton calls
+-- Otherwise either SENT comes first then function calls, or some function calls then SENT then more function calls
+local castTarget, castID, mouseoverGUID, mouseoverName, hadTargetingCursor, lastSentID, lastTargetGUID, lastTargetName
+local lastFriendlyGUID, lastFriendlyName, lastGUID, lastName, lastIsFriend
+local castGUIDs, guidPriorities = {}, {}
+
+-- Deals with the fact that functions are called differently
+-- Why a table when you can only cast one spell at a time you ask? When you factor in lag and mash clicking it's possible to:
+-- cast A, interrupt it, cast B and have A fire SUCEEDED before B does, the tables keeps it from bugging out
+local function setCastData(priority, name, guid)
+ if( not guid or not lastSentID ) then return end
+ if( guidPriorities[lastSentID] and guidPriorities[lastSentID] >= priority ) then return end
+
+ -- This is meant as a way of locking a cast in because which function has accurate data can be called into question at times, one of them always does though
+ -- this means that as soon as it finds a name match it locks the GUID in until another SENT is fired. Technically it's possible to get a bad GUID but it first requires
+ -- the functions to return different data and it requires the messed up call to be for another name conflict.
+ if( castTarget and castTarget == name ) then priority = 99 end
+
+ castGUIDs[lastSentID] = guid
+ guidPriorities[lastSentID] = priority
+end
+
+-- When the game tries to figure out the UnitID from the name it will prioritize players over non-players
+-- if there are conflicts in names it will pull the one with the least amount of current health
+function HealComm:UNIT_SPELLCAST_SENT(unit, spellName, spellRank, castOn)
+ if( unit ~= "player" or not spellData[spellName] or not averageHeal[spellName][spellRank] ) then return end
+
+ castTarget = gsub(castOn, "(.-)%-(.*)$", "%1")
+ lastSentID = spellName .. spellRank
+
+ -- Self cast is off which means it's possible to have a spell waiting for a target.
+ -- It's possible that it's the mouseover unit, but if a Target, TargetLast or AssistUnit call comes right after it means it's casting on that instead instead.
+ if( hadTargetingCursor ) then
+ hadTargetingCursor = nil
+ self.resetFrame:Show()
+
+ guidPriorities[lastSentID] = nil
+ setCastData(5, mouseoverName, mouseoverGUID)
+ else
+ -- If the player is ungrouped and healing, you can't take advantage of the name -> "unit" map, look in the UnitIDs that would most likely contain the information that's needed.
+ local guid = UnitGUID(castOn)
+ if( not guid ) then
+ guid = UnitName("target") == castTarget and UnitGUID("target") or UnitName("focus") == castTarget and UnitGUID("focus") or UnitName("mouseover") == castTarget and UnitGUID("mouseover") or UnitName("targettarget") == castTarget and UnitGUID("target") or UnitName("focustarget") == castTarget and UnitGUID("focustarget")
+ end
+
+ guidPriorities[lastSentID] = nil
+ setCastData(0, nil, guid)
+ end
+end
+
+function HealComm:UNIT_SPELLCAST_START(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or not averageHeal[spellName][spellRank] or UnitIsCharmed("player") or not UnitPlayerControlled("player") ) then return end
+ local nameID = spellName .. spellRank
+ local castGUID = castGUIDs[nameID]
+ if( not castGUID ) then
+ return
+ end
+
+ castID = id
+
+ -- Figure out who we are healing and for how much
+ local type, amount, ticks, localTicks = CalculateHealing(castGUID, spellName, spellRank)
+ local targets, amount = GetHealTargets(type, castGUID, max(amount, 0), spellName)
+
+ if( type == DIRECT_HEALS ) then
+ parseDirectHeal(playerGUID, self.spellToID[nameID], amount, split(",", targets))
+ sendMessage(format("D::%d:%d:%s", self.spellToID[nameID] or 0, amount or "", targets))
+ elseif( type == CHANNEL_HEALS ) then
+ parseChannelHeal(playerGUID, self.spellToID[nameID], amount, localTicks, split(",", targets))
+ sendMessage(format("C::%d:%d:%s:%s", self.spellToID[nameID] or 0, amount, ticks, targets))
+ end
+end
+
+HealComm.UNIT_SPELLCAST_CHANNEL_START = HealComm.UNIT_SPELLCAST_START
+
+function HealComm:UNIT_SPELLCAST_SUCCEEDED(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or id ~= castID or id == 0 ) then return end
+ local nameID = spellName .. spellRank
+
+ castID = nil
+ parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], false)
+ sendMessage(format("S::%d:0", self.spellToID[nameID] or 0))
+end
+
+function HealComm:UNIT_SPELLCAST_STOP(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or id ~= castID ) then return end
+ local nameID = spellName .. spellRank
+
+ castID = nil
+ parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], true)
+ sendMessage(format("S::%d:1", self.spellToID[nameID] or 0))
+end
+
+function HealComm:UNIT_SPELLCAST_CHANNEL_STOP(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or id ~= castID ) then return end
+ local nameID = spellName .. spellRank
+
+ castID = nil
+ parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], false)
+ sendMessage(format("S::%d:0", self.spellToID[nameID] or 0))
+end
+
+-- Cast didn't go through, recheck any charge data if necessary
+function HealComm:UNIT_SPELLCAST_INTERRUPTED(unit, spellName, spellRank, id)
+ if( unit ~= "player" or not spellData[spellName] or castID ~= id ) then return end
+
+ local guid = castGUIDs[spellName .. spellRank]
+ if( guid ) then
+ ResetChargeData(guid, spellName, spellRank)
+ end
+end
+
+-- It's faster to do heal delays locally rather than through syncing, as it only has to go from WoW -> Player instead of Caster -> WoW -> Player
+function HealComm:UNIT_SPELLCAST_DELAYED(unit, spellName, spellRank, id)
+ local casterGUID = UnitGUID(unit)
+ if( unit == "focus" or unit == "target" or not pendingHeals[casterGUID] or not pendingHeals[casterGUID][spellName] ) then return end
+
+ -- Direct heal delayed
+ if( pendingHeals[casterGUID][spellName].bitType == DIRECT_HEALS ) then
+ local startTime, endTime = select(5, UnitCastingInfo(unit))
+ if( startTime and endTime ) then
+ parseHealDelayed(casterGUID, startTime / 1000, endTime / 1000, spellName)
+ end
+ -- Channel heal delayed
+ elseif( pendingHeals[casterGUID][spellName].bitType == CHANNEL_HEALS ) then
+ local startTime, endTime = select(5, UnitChannelInfo(unit))
+ if( startTime and endTime ) then
+ parseHealDelayed(casterGUID, startTime / 1000, endTime / 1000, spellName)
+ end
+ end
+end
+
+HealComm.UNIT_SPELLCAST_CHANNEL_UPDATE = HealComm.UNIT_SPELLCAST_DELAYED
+
+-- Need to keep track of mouseover as it can change in the split second after/before casts
+function HealComm:UPDATE_MOUSEOVER_UNIT()
+ mouseoverGUID = UnitCanAssist("player", "mouseover") and UnitGUID("mouseover")
+ mouseoverName = UnitCanAssist("player", "mouseover") and UnitName("mouseover")
+end
+
+-- Keep track of our last target/friendly target for the sake of /targetlast and /targetlastfriend
+function HealComm:PLAYER_TARGET_CHANGED()
+ if( lastGUID and lastName ) then
+ if( lastIsFriend ) then
+ lastFriendlyGUID, lastFriendlyName = lastGUID, lastName
+ end
+
+ lastTargetGUID, lastTargetName = lastGUID, lastName
+ end
+
+ -- Despite the fact that it's called target last friend, UnitIsFriend won't actually work
+ lastGUID = UnitGUID("target")
+ lastName = UnitName("target")
+ lastIsFriend = UnitCanAssist("player", "target")
+end
+
+-- Unit was targeted through a function
+function HealComm:Target(unit)
+ if( self.resetFrame:IsShown() and UnitCanAssist("player", unit) ) then
+ setCastData(6, UnitName(unit), UnitGUID(unit))
+ end
+
+ self.resetFrame:Hide()
+ hadTargetingCursor = nil
+end
+
+-- This is only needed when auto self cast is off, in which case this is called right after UNIT_SPELLCAST_SENT
+-- because the player got a waiting-for-cast icon up and they pressed a key binding to target someone
+HealComm.TargetUnit = HealComm.Target
+
+-- Works the same as the above except it's called when you have a cursor icon and you click on a secure frame with a target attribute set
+HealComm.SpellTargetUnit = HealComm.Target
+
+-- Used in /assist macros
+function HealComm:AssistUnit(unit)
+ if( self.resetFrame:IsShown() and UnitCanAssist("player", unit .. "target") ) then
+ setCastData(6, UnitName(unit .. "target"), UnitGUID(unit .. "target"))
+ end
+
+ self.resetFrame:Hide()
+ hadTargetingCursor = nil
+end
+
+-- Target last was used, the only reason this is called with reset frame being shown is we're casting on a valid unit
+-- don't have to worry about the GUID no longer being invalid etc
+function HealComm:TargetLast(guid, name)
+ if( name and guid and self.resetFrame:IsShown() ) then
+ setCastData(6, name, guid)
+ end
+
+ self.resetFrame:Hide()
+ hadTargetingCursor = nil
+end
+
+function HealComm:TargetLastFriend()
+ self:TargetLast(lastFriendlyGUID, lastFriendlyName)
+end
+
+function HealComm:TargetLastTarget()
+ self:TargetLast(lastTargetGUID, lastTargetName)
+end
+
+-- Spell was cast somehow
+function HealComm:CastSpell(arg, unit)
+ -- If the spell is waiting for a target and it's a spell action button then we know that the GUID has to be mouseover or a key binding cast.
+ if( unit and UnitCanAssist("player", unit) ) then
+ setCastData(4, UnitName(unit), UnitGUID(unit))
+ -- No unit, or it's a unit we can't assist
+ elseif( not SpellIsTargeting() ) then
+ if( UnitCanAssist("player", "target") ) then
+ setCastData(4, UnitName("target"), UnitGUID("target"))
+ else
+ setCastData(4, playerName, playerGUID)
+ end
+
+ hadTargetingCursor = nil
+ else
+ hadTargetingCursor = true
+ end
+end
+
+HealComm.CastSpellByName = HealComm.CastSpell
+HealComm.CastSpellByID = HealComm.CastSpell
+HealComm.UseAction = HealComm.CastSpell
+
+-- Make sure we don't have invalid units in this
+local function sanityCheckMapping()
+ for guid, unit in pairs(guidToUnit) do
+ -- Unit no longer exists, remove all healing for them
+ if( not UnitExists(unit) ) then
+ -- Check for (and remove) any active heals
+ if( pendingHeals[guid] ) then
+ for id, pending in pairs(pendingHeals[guid]) do
+ if( pending.bitType ) then
+ parseHealEnd(guid, pending, nil, pending.spellID, true)
+ end
+ end
+
+ pendingHeals[guid] = nil
+ end
+
+ -- Remove any heals that are on them
+ removeAllRecords(guid)
+
+ guidToUnit[guid] = nil
+ guidToGroup[guid] = nil
+ end
+ end
+end
+
+-- 5s poll that tries to solve the problem of X running out of range while a HoT is ticking
+-- this is not really perfect far from it in fact. If I can find a better solution I will switch to that.
+if( not HealComm.hotMonitor ) then
+ HealComm.hotMonitor = CreateFrame("Frame")
+ HealComm.hotMonitor:Hide()
+ HealComm.hotMonitor.timeElapsed = 0
+ HealComm.hotMonitor:SetScript("OnUpdate", function(self, elapsed)
+ self.timeElapsed = self.timeElapsed + elapsed
+ if( self.timeElapsed < 5 ) then return end
+ self.timeElapsed = self.timeElapsed - 5
+
+ -- For the time being, it will only remove them if they don't exist and it found a valid unit
+ -- units that leave the raid are automatically removed
+ local found
+ for guid in pairs(activeHots) do
+ if( guidToUnit[guid] and not UnitIsVisible(guidToUnit[guid]) ) then
+ removeAllRecords(guid)
+ else
+ found = true
+ end
+ end
+
+ if( not found ) then
+ self:Hide()
+ end
+ end)
+end
+
+-- After the player leaves a group, tables are wiped out or released for GC
+local wasInParty, wasInRaid
+local function clearGUIDData()
+ clearPendingHeals()
+
+ twipe(compressGUID)
+ twipe(decompressGUID)
+ twipe(activePets)
+
+ playerGUID = playerGUID or UnitGUID("player")
+ HealComm.guidToUnit = {[playerGUID] = "player"}
+ guidToUnit = HealComm.guidToUnit
+
+ HealComm.guidToGroup = {}
+ guidToGroup = HealComm.guidToGroup
+
+ HealComm.activeHots = {}
+ activeHots = HealComm.activeHots
+
+ HealComm.pendingHeals = {}
+ pendingHeals = HealComm.pendingHeals
+
+ HealComm.bucketHeals = {}
+ bucketHeals = HealComm.bucketHeals
+
+ wasInParty, wasInRaid = nil, nil
+end
+
+-- Keeps track of pet GUIDs, as pets are considered vehicles this will also map vehicle GUIDs to unit
+function HealComm:UNIT_PET(unit)
+ local pet = self.unitToPet[unit]
+ local guid = pet and UnitGUID(pet)
+
+ -- We have an active pet guid from this user and it's different, kill it
+ local activeGUID = activePets[unit]
+ if( activeGUID and activeGUID ~= guid ) then
+ removeAllRecords(activeGUID)
+
+ guidToUnit[activeGUID] = nil
+ guidToGroup[activeGUID] = nil
+ activePets[unit] = nil
+ end
+
+ -- Add the new record
+ if( guid ) then
+ guidToUnit[guid] = pet
+ guidToGroup[guid] = guidToGroup[UnitGUID(unit)]
+ activePets[unit] = guid
+ end
+end
+
+-- Keep track of party GUIDs, ignored in raids as RRU will handle that mapping
+function HealComm:PARTY_MEMBERS_CHANGED()
+ if( GetNumRaidMembers() > 0 ) then return end
+ updateDistributionChannel()
+
+ if( GetNumPartyMembers() == 0 ) then
+ if( wasInParty ) then
+ clearGUIDData()
+ end
+ return
+ end
+
+ -- Parties are not considered groups in terms of API, so fake it and pretend they are all in group 0
+ guidToGroup[playerGUID or UnitGUID("player")] = 0
+ if( not wasInParty ) then self:UNIT_PET("player") end
+
+ for i=1, MAX_PARTY_MEMBERS do
+ local unit = "party" .. i
+ if( UnitExists(unit) ) then
+ local guid = UnitGUID(unit)
+ local lastGroup = guidToGroup[guid]
+ guidToUnit[guid] = unit
+ guidToGroup[guid] = 0
+
+ if( not wasInParty or lastGroup ~= guidToGroup[guid] ) then
+ self:UNIT_PET(unit)
+ end
+ end
+ end
+
+ sanityCheckMapping()
+ wasInParty = true
+end
+
+-- Keep track of raid GUIDs
+function HealComm:RAID_ROSTER_UPDATE()
+ updateDistributionChannel()
+
+ -- Left raid, clear any cache we had
+ if( GetNumRaidMembers() == 0 ) then
+ if( wasInRaid ) then
+ clearGUIDData()
+ end
+ return
+ end
+
+ -- Add new members
+ for i=1, MAX_RAID_MEMBERS do
+ local unit = "raid" .. i
+ if( UnitExists(unit) ) then
+ local guid = UnitGUID(unit)
+ local lastGroup = guidToGroup[guid]
+ guidToUnit[guid] = unit
+ guidToGroup[guid] = select(3, GetRaidRosterInfo(i))
+
+ -- If the pets owners group changed then the pets group should be updated too
+ if( not wasInRaid or guidToGroup[guid] ~= lastGroup ) then
+ self:UNIT_PET(unit)
+ end
+ end
+ end
+
+ sanityCheckMapping()
+ wasInRaid = true
+end
+
+-- Keep track of Arena GUIDs (NEW)
+function HealComm:ARENA_OPPONENT_UPDATE(unit, type)
+ updateDistributionChannel()
+
+ -- Add new members
+ if(type == "seen") then
+ if( UnitExists(unit) ) then
+ if( not(string.find('pet', unit)) ) then
+ local guid = UnitGUID(unit)
+ guidToUnit[guid] = unit
+ guidToGroup[guid] = 0
+ self:UNIT_PET(unit)
+ print('added new arena')
+ end
+ end
+ end
+
+end
+
+-- PLAYER_ALIVE = got talent data
+function HealComm:PLAYER_ALIVE()
+ self:PLAYER_TALENT_UPDATE()
+ self.eventFrame:UnregisterEvent("PLAYER_ALIVE")
+end
+
+-- Initialize the library
+function HealComm:OnInitialize()
+ -- If another instance already loaded then the tables should be wiped to prevent old data from persisting
+ -- in case of a spell being removed later on, only can happen if a newer LoD version is loaded
+ twipe(spellData)
+ twipe(hotData)
+ twipe(itemSetsData)
+ twipe(talentData)
+ twipe(averageHeal)
+
+ -- Load all of the classes formulas and such
+ LoadClassData()
+
+ -- Setup the metatables for average healing
+ for spell in pairs(spellData) do
+ averageHeal[spell] = setmetatable({spell = spell}, self.averageHealMT)
+ end
+
+ -- Cache glyphs initially
+ for id=1, GetNumGlyphSockets() do
+ local enabled, _, glyphID = GetGlyphSocketInfo(id)
+ if( enabled and glyphID ) then
+ glyphCache[glyphID] = true
+ glyphCache[id] = glyphID
+ end
+ end
+
+ self:PLAYER_EQUIPMENT_CHANGED()
+
+ -- When first logging in talent data isn't available until at least PLAYER_ALIVE, so if we don't have data
+ -- will wait for that event otherwise will just cache it right now
+ if( GetNumTalentTabs() == 0 ) then
+ self.eventFrame:RegisterEvent("PLAYER_ALIVE")
+ else
+ self:PLAYER_TALENT_UPDATE()
+ end
+
+ if( ResetChargeData ) then
+ HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
+ end
+
+ -- Finally, register it all
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_SENT")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_START")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_STOP")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
+ self.eventFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
+ self.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ self.eventFrame:RegisterEvent("PLAYER_TALENT_UPDATE")
+ self.eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+ self.eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
+ self.eventFrame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
+ self.eventFrame:RegisterEvent("PLAYER_LEVEL_UP")
+ self.eventFrame:RegisterEvent("GLYPH_ADDED")
+ self.eventFrame:RegisterEvent("GLYPH_REMOVED")
+ self.eventFrame:RegisterEvent("GLYPH_UPDATED")
+ self.eventFrame:RegisterEvent("UNIT_AURA")
+
+ if( self.initialized ) then return end
+ self.initialized = true
+
+ self.resetFrame = CreateFrame("Frame")
+ self.resetFrame:Hide()
+ self.resetFrame:SetScript("OnUpdate", function(self) self:Hide() end)
+
+ -- You can't unhook secure hooks after they are done, so will hook once and the HealComm table will update with the latest functions
+ -- automagically. If a new function is ever used it'll need a specific variable to indicate those set of hooks.
+ -- By default most of these are mapped to a more generic function, but I call separate ones so I don't have to rehook
+ -- if it turns out I need to know something specific
+ hooksecurefunc("TargetUnit", function(...) HealComm:TargetUnit(...) end)
+ hooksecurefunc("SpellTargetUnit", function(...) HealComm:SpellTargetUnit(...) end)
+ hooksecurefunc("AssistUnit", function(...) HealComm:AssistUnit(...) end)
+ hooksecurefunc("UseAction", function(...) HealComm:UseAction(...) end)
+ hooksecurefunc("TargetLastFriend", function(...) HealComm:TargetLastFriend(...) end)
+ hooksecurefunc("TargetLastTarget", function(...) HealComm:TargetLastTarget(...) end)
+ hooksecurefunc("CastSpellByName", function(...) HealComm:CastSpellByName(...) end)
+
+ -- Fixes hook error for people who are not on 3.2 yet
+ if( CastSpellByID ) then
+ hooksecurefunc("CastSpellByID", function(...) HealComm:CastSpellByID(...) end)
+ end
+end
+
+-- General event handler
+local function OnEvent(self, event, ...)
+ HealComm[event](HealComm, ...)
+end
+
+-- Event handler
+HealComm.eventFrame = HealComm.frame or HealComm.eventFrame or CreateFrame("Frame")
+HealComm.eventFrame:UnregisterAllEvents()
+HealComm.eventFrame:RegisterEvent("UNIT_PET")
+HealComm.eventFrame:SetScript("OnEvent", OnEvent)
+HealComm.frame = nil
+
+-- At PLAYER_LEAVING_WORLD (Actually more like MIRROR_TIMER_STOP but anyway) UnitGUID("player") returns nil, delay registering
+-- events and set a playerGUID/playerName combo for all players on PLAYER_LOGIN not just the healers.
+function HealComm:PLAYER_LOGIN()
+ playerGUID = UnitGUID("player")
+ playerName = UnitName("player")
+ playerLevel = UnitLevel("player")
+
+ -- Oddly enough player GUID is not available on file load, so keep the map of player GUID to themselves too
+ guidToUnit[playerGUID] = "player"
+
+ if( isHealerClass ) then
+ self:OnInitialize()
+ end
+
+ self.eventFrame:UnregisterEvent("PLAYER_LOGIN")
+ self.eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
+ self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ self.eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
+ self.eventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
+ self.eventFrame:RegisterEvent("ARENA_OPPONENT_UPDATE")
+
+ self:ZONE_CHANGED_NEW_AREA()
+ self:RAID_ROSTER_UPDATE()
+ self:PARTY_MEMBERS_CHANGED()
+end
+
+if( not IsLoggedIn() ) then
+ HealComm.eventFrame:RegisterEvent("PLAYER_LOGIN")
+else
+ HealComm:PLAYER_LOGIN()
+end
\ No newline at end of file
diff --git a/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.toc b/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.toc
new file mode 100644
index 0000000..ae37318
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.toc
@@ -0,0 +1,14 @@
+## Interface: 30300
+## Title: Lib: HealCommArena-4.0
+## Notes: Library for showing incoming heals and hots of arena enemy player
+## Author: Shadowed
+## X-Category: Library
+## X-Curse-Packaged-Version: v1.6.6
+## X-Curse-Project-Name: LibHealCommArena-4.0
+## X-Curse-Project-ID: libhealcommarena-4-0
+## X-Curse-Repository-ID: wow/libhealcommarena-4-0/mainline
+
+libs\LibStub\LibStub.lua
+libs\CallbackHandler-1.0\CallbackHandler-1.0\CallbackHandler-1.0.xml
+
+LibHealCommArena-4.0.xml
diff --git a/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.xml b/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.xml
new file mode 100644
index 0000000..995dde8
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/LibHealCommArena-4.0.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0.toc b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0.toc
new file mode 100644
index 0000000..04c06f2
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0.toc
@@ -0,0 +1,15 @@
+## Interface: 30200
+## LoadOnDemand: 1
+## Title: Lib: CallbackHandler-1.0
+## Notes: Callback handling Library, normally embedded inside other libraries
+## Author: Ace3 Development Team
+## X-Website: http://www.wowace.com/projects/callbackhandler/
+## X-Category: Library
+## X-License: BSD-2.0
+## X-Curse-Packaged-Version: 1.0.5
+## X-Curse-Project-Name: CallbackHandler-1.0
+## X-Curse-Project-ID: callbackhandler
+## X-Curse-Repository-ID: wow/callbackhandler/mainline
+
+LibStub\LibStub.lua
+CallbackHandler-1.0\CallbackHandler-1.0.xml
diff --git a/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua
new file mode 100644
index 0000000..3dea01a
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.lua
@@ -0,0 +1,239 @@
+--[[ $Id: CallbackHandler-1.0.lua 9 2009-09-17 10:38:10Z mikk $ ]]
+local MAJOR, MINOR = "CallbackHandler-1.0", 5
+local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not CallbackHandler then return end -- No upgrade needed
+
+local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
+
+local type = type
+local pcall = pcall
+local pairs = pairs
+local assert = assert
+local concat = table.concat
+local loadstring = loadstring
+local next = next
+local select = select
+local type = type
+local xpcall = xpcall
+
+local function errorhandler(err)
+ return geterrorhandler()(err)
+end
+
+local function CreateDispatcher(argCount)
+ local code = [[
+ local next, xpcall, eh = ...
+
+ local method, ARGS
+ local function call() method(ARGS) end
+
+ local function dispatch(handlers, ...)
+ local index
+ index, method = next(handlers)
+ if not method then return end
+ local OLD_ARGS = ARGS
+ ARGS = ...
+ repeat
+ xpcall(call, eh)
+ index, method = next(handlers, index)
+ until not method
+ ARGS = OLD_ARGS
+ end
+
+ return dispatch
+ ]]
+
+ local ARGS, OLD_ARGS = {}, {}
+ for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
+ code = code:gsub("OLD_ARGS", concat(OLD_ARGS, ", ")):gsub("ARGS", concat(ARGS, ", "))
+ return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
+end
+
+local Dispatchers = setmetatable({}, {__index=function(self, argCount)
+ local dispatcher = CreateDispatcher(argCount)
+ rawset(self, argCount, dispatcher)
+ return dispatcher
+end})
+
+--------------------------------------------------------------------------
+-- CallbackHandler:New
+--
+-- target - target object to embed public APIs in
+-- RegisterName - name of the callback registration API, default "RegisterCallback"
+-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
+-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
+
+function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName, OnUsed, OnUnused)
+ -- TODO: Remove this after beta has gone out
+ assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused")
+
+ RegisterName = RegisterName or "RegisterCallback"
+ UnregisterName = UnregisterName or "UnregisterCallback"
+ if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
+ UnregisterAllName = "UnregisterAllCallbacks"
+ end
+
+ -- we declare all objects and exported APIs inside this closure to quickly gain access
+ -- to e.g. function names, the "target" parameter, etc
+
+
+ -- Create the registry object
+ local events = setmetatable({}, meta)
+ local registry = { recurse=0, events=events }
+
+ -- registry:Fire() - fires the given event/message into the registry
+ function registry:Fire(eventname, ...)
+ if not rawget(events, eventname) or not next(events[eventname]) then return end
+ local oldrecurse = registry.recurse
+ registry.recurse = oldrecurse + 1
+
+ Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
+
+ registry.recurse = oldrecurse
+
+ if registry.insertQueue and oldrecurse==0 then
+ -- Something in one of our callbacks wanted to register more callbacks; they got queued
+ for eventname,callbacks in pairs(registry.insertQueue) do
+ local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
+ for self,func in pairs(callbacks) do
+ events[eventname][self] = func
+ -- fire OnUsed callback?
+ if first and registry.OnUsed then
+ registry.OnUsed(registry, target, eventname)
+ first = nil
+ end
+ end
+ end
+ registry.insertQueue = nil
+ end
+ end
+
+ -- Registration of a callback, handles:
+ -- self["method"], leads to self["method"](self, ...)
+ -- self with function ref, leads to functionref(...)
+ -- "addonId" (instead of self) with function ref, leads to functionref(...)
+ -- all with an optional arg, which, if present, gets passed as first argument (after self if present)
+ target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
+ if type(eventname) ~= "string" then
+ error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
+ end
+
+ method = method or eventname
+
+ local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
+
+ if type(method) ~= "string" and type(method) ~= "function" then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
+ end
+
+ local regfunc
+
+ if type(method) == "string" then
+ -- self["method"] calling style
+ if type(self) ~= "table" then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
+ elseif self==target then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
+ elseif type(self[method]) ~= "function" then
+ error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
+ end
+
+ if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
+ local arg=select(1,...)
+ regfunc = function(...) self[method](self,arg,...) end
+ else
+ regfunc = function(...) self[method](self,...) end
+ end
+ else
+ -- function ref with self=object or self="addonId"
+ if type(self)~="table" and type(self)~="string" then
+ error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string expected.", 2)
+ end
+
+ if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
+ local arg=select(1,...)
+ regfunc = function(...) method(arg,...) end
+ else
+ regfunc = method
+ end
+ end
+
+
+ if events[eventname][self] or registry.recurse<1 then
+ -- if registry.recurse<1 then
+ -- we're overwriting an existing entry, or not currently recursing. just set it.
+ events[eventname][self] = regfunc
+ -- fire OnUsed callback?
+ if registry.OnUsed and first then
+ registry.OnUsed(registry, target, eventname)
+ end
+ else
+ -- we're currently processing a callback in this registry, so delay the registration of this new entry!
+ -- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
+ registry.insertQueue = registry.insertQueue or setmetatable({},meta)
+ registry.insertQueue[eventname][self] = regfunc
+ end
+ end
+
+ -- Unregister a callback
+ target[UnregisterName] = function(self, eventname)
+ if not self or self==target then
+ error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
+ end
+ if type(eventname) ~= "string" then
+ error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
+ end
+ if rawget(events, eventname) and events[eventname][self] then
+ events[eventname][self] = nil
+ -- Fire OnUnused callback?
+ if registry.OnUnused and not next(events[eventname]) then
+ registry.OnUnused(registry, target, eventname)
+ end
+ end
+ if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
+ registry.insertQueue[eventname][self] = nil
+ end
+ end
+
+ -- OPTIONAL: Unregister all callbacks for given selfs/addonIds
+ if UnregisterAllName then
+ target[UnregisterAllName] = function(...)
+ if select("#",...)<1 then
+ error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
+ end
+ if select("#",...)==1 and ...==target then
+ error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
+ end
+
+
+ for i=1,select("#",...) do
+ local self = select(i,...)
+ if registry.insertQueue then
+ for eventname, callbacks in pairs(registry.insertQueue) do
+ if callbacks[self] then
+ callbacks[self] = nil
+ end
+ end
+ end
+ for eventname, callbacks in pairs(events) do
+ if callbacks[self] then
+ callbacks[self] = nil
+ -- Fire OnUnused callback?
+ if registry.OnUnused and not next(callbacks) then
+ registry.OnUnused(registry, target, eventname)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return registry
+end
+
+
+-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
+-- try to upgrade old implicit embeds since the system is selfcontained and
+-- relies on closures to work.
+
diff --git a/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml
new file mode 100644
index 0000000..876df83
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/CallbackHandler-1.0/CallbackHandler-1.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/LibStub/LibStub.lua b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/LibStub/LibStub.lua
new file mode 100644
index 0000000..0a41ac0
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/libs/CallbackHandler-1.0/LibStub/LibStub.lua
@@ -0,0 +1,30 @@
+-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
+-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
+local LibStub = _G[LIBSTUB_MAJOR]
+
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+ LibStub = LibStub or {libs = {}, minors = {} }
+ _G[LIBSTUB_MAJOR] = LibStub
+ LibStub.minor = LIBSTUB_MINOR
+
+ function LibStub:NewLibrary(major, minor)
+ assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
+ minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
+
+ local oldminor = self.minors[major]
+ if oldminor and oldminor >= minor then return nil end
+ self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+ return self.libs[major], oldminor
+ end
+
+ function LibStub:GetLibrary(major, silent)
+ if not self.libs[major] and not silent then
+ error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
+ end
+ return self.libs[major], self.minors[major]
+ end
+
+ function LibStub:IterateLibraries() return pairs(self.libs) end
+ setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
diff --git a/Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.lua b/Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.lua
new file mode 100644
index 0000000..0a41ac0
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.lua
@@ -0,0 +1,30 @@
+-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info
+-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
+local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
+local LibStub = _G[LIBSTUB_MAJOR]
+
+if not LibStub or LibStub.minor < LIBSTUB_MINOR then
+ LibStub = LibStub or {libs = {}, minors = {} }
+ _G[LIBSTUB_MAJOR] = LibStub
+ LibStub.minor = LIBSTUB_MINOR
+
+ function LibStub:NewLibrary(major, minor)
+ assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
+ minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
+
+ local oldminor = self.minors[major]
+ if oldminor and oldminor >= minor then return nil end
+ self.minors[major], self.libs[major] = minor, self.libs[major] or {}
+ return self.libs[major], oldminor
+ end
+
+ function LibStub:GetLibrary(major, silent)
+ if not self.libs[major] and not silent then
+ error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
+ end
+ return self.libs[major], self.minors[major]
+ end
+
+ function LibStub:IterateLibraries() return pairs(self.libs) end
+ setmetatable(LibStub, { __call = LibStub.GetLibrary })
+end
diff --git a/Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.toc b/Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.toc
new file mode 100644
index 0000000..17cf732
--- /dev/null
+++ b/Gladius/libs/LibHealCommArena-4.0/libs/LibStub/LibStub.toc
@@ -0,0 +1,13 @@
+## Interface: 20400
+## Title: Lib: LibStub
+## Notes: Universal Library Stub
+## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel
+## X-Website: http://jira.wowace.com/browse/LS
+## X-Category: Library
+## X-License: Public Domain
+## X-Curse-Packaged-Version: 1.0
+## X-Curse-Project-Name: LibStub
+## X-Curse-Project-ID: libstub
+## X-Curse-Repository-ID: wow/libstub/mainline
+
+LibStub.lua
diff --git a/Gladius/libs/LibStub/LibStub.lua b/Gladius/libs/LibStub/LibStub.lua
index 0a41ac0..51ad7d2 100644
--- a/Gladius/libs/LibStub/LibStub.lua
+++ b/Gladius/libs/LibStub/LibStub.lua
@@ -7,24 +7,27 @@ if not LibStub or LibStub.minor < LIBSTUB_MINOR then
LibStub = LibStub or {libs = {}, minors = {} }
_G[LIBSTUB_MAJOR] = LibStub
LibStub.minor = LIBSTUB_MINOR
-
+
function LibStub:NewLibrary(major, minor)
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
-
+
local oldminor = self.minors[major]
- if oldminor and oldminor >= minor then return nil end
+ if oldminor and oldminor >= minor then
+ print(oldminor, minor)
+ return nil
+ end
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
return self.libs[major], oldminor
end
-
+
function LibStub:GetLibrary(major, silent)
if not self.libs[major] and not silent then
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
end
return self.libs[major], self.minors[major]
end
-
+
function LibStub:IterateLibraries() return pairs(self.libs) end
setmetatable(LibStub, { __call = LibStub.GetLibrary })
end
diff --git a/Gladius/localization/enUS.lua b/Gladius/localization/enUS.lua
index 35801a7..57a3f15 100644
--- a/Gladius/localization/enUS.lua
+++ b/Gladius/localization/enUS.lua
@@ -400,6 +400,7 @@ L["Cooldowns"] = "Cooldowns"
L["Cooldown position"] = "Cooldown position"
L["Position of the cooldown icons"] = "Position of the cooldown icons"
L["Show cooldown icons"] = "Show cooldown icons"
+L["Show cooldown icons on one line"] = "Show cooldown icons on one line"
L["Cooldown list"] = "Cooldown list"
L["Show cooldown"] = "Show cooldown"
L["Hide Cooldown border"] = "Hide Cooldown border"
diff --git a/Gladius/options.lua b/Gladius/options.lua
index 867d34f..50ca9ba 100644
--- a/Gladius/options.lua
+++ b/Gladius/options.lua
@@ -157,6 +157,7 @@ local defaults = {
smoothingAmount = 0.33,
cutawayBar = false,
cooldown = false,
+ cooldownOneLine = false,
cooldownIconPadding = 2,
cooldownIconMargin = 2,
hideCooldownBorder = false,
@@ -227,6 +228,7 @@ local function slashHandler(option)
self:Print(L["Valid slash commands are:"])
self:Print(L["/gladius ui"])
self:Print(L["/gladius test1-5"])
+ self:Print(L["/gladius trinket"])
self:Print(L["/gladius hide"])
end
end
@@ -839,6 +841,12 @@ function Gladius:SetupOptions()
desc=L["Bar settings"],
order=2,
args = {
+ castBar = {
+ type="toggle",
+ name=L["Show cast bars"],
+ desc=L["Show cast bars"],
+ order=0,
+ },
castBarPos = {
type="select",
name=L["Cast bar position"],
@@ -848,14 +856,8 @@ function Gladius:SetupOptions()
["RIGHT"] = L["Right"],
["LEFT"] = L["Left"],
},
- order=4,
+ order=2,
},
- castBar = {
- type="toggle",
- name=L["Show cast bars"],
- desc=L["Show cast bars"],
- order=1,
- },
castBarSpark = {
type="toggle",
name=L["Show cast bars spark"],
@@ -1897,6 +1899,11 @@ function Gladius:SetupOptions()
name=L["Show cooldown icons"],
order=0,
},
+ cooldownOneLine = {
+ type="toggle",
+ name=L["Show cooldown icons on one line"],
+ order=1,
+ },
cooldownPos = {
type="select",
name=L["Cooldown position"],
@@ -1905,7 +1912,7 @@ function Gladius:SetupOptions()
["RIGHT"] = L["Right"],
["LEFT"] = L["Left"],
},
- order=1,
+ order=2,
},
hideCooldownBorder = {
type="toggle",
@@ -1916,7 +1923,7 @@ function Gladius:SetupOptions()
type="range",
name=L["Cooldown icon margin"],
min=0,
- max=13,
+ max=35,
step=0.1,
order=6,
},
@@ -1924,7 +1931,7 @@ function Gladius:SetupOptions()
type="range",
name=L["Cooldown icon padding"],
min=0,
- max=8,
+ max=13,
step=0.1,
order=7,
},
From 85c8c9cec4aa55e04a780030b3c597f9241a0ab4 Mon Sep 17 00:00:00 2001
From: Cortes-Jeremy <56119078+Cortes-Jeremy@users.noreply.github.com>
Date: Sat, 1 May 2021 11:30:13 +0200
Subject: [PATCH 2/3] Added Minimap Icon to toggle test frames and window ui
---
Gladius/Gladius.lua | 29 ++
Gladius/Gladius.toc | 2 +-
Gladius/embeds.xml | 2 +
Gladius/libs/LibDBIcon-1.0/LibDBIcon-1.0.lua | 264 ++++++++++++++++++
.../LibDataBroker-1.1/LibDataBroker-1.1.lua | 90 ++++++
Gladius/libs/LibDataBroker-1.1/README.textile | 13 +
Gladius/media/Icons/Gladius.blp | Bin 0 -> 3916 bytes
Gladius/options.lua | 1 +
8 files changed, 400 insertions(+), 1 deletion(-)
create mode 100644 Gladius/libs/LibDBIcon-1.0/LibDBIcon-1.0.lua
create mode 100644 Gladius/libs/LibDataBroker-1.1/LibDataBroker-1.1.lua
create mode 100644 Gladius/libs/LibDataBroker-1.1/README.textile
create mode 100644 Gladius/media/Icons/Gladius.blp
diff --git a/Gladius/Gladius.lua b/Gladius/Gladius.lua
index e6c6cb5..2d3c5d8 100644
--- a/Gladius/Gladius.lua
+++ b/Gladius/Gladius.lua
@@ -4,6 +4,8 @@ local LSM = LibStub("LibSharedMedia-3.0")
local LCG = LibStub("LibCustomGlow-1.0")
local LHC = LibStub("LibHealCommArena-4.0")
local LAM = LibStub:GetLibrary("AbsorbsMonitor-1.0", true)
+local LDB = LibStub ("LibDataBroker-1.1")
+local LDBIcon = LibStub("LibDBIcon-1.0")
local arenaUnits = {}
local arenaGUID = {}
@@ -96,6 +98,9 @@ function Gladius:OnInitialize()
self.drTime = { "50%", "25%", L["immune"] }
self:SetupOptions()
+
+ -- Create Minimap Icon
+ self:CreateMinimapButton()
end
function Gladius:OnEnable()
@@ -129,6 +134,30 @@ function Gladius:OnProfileChanged(event, database, newProfileKey)
self:ToggleFrame(5)
end
+function Gladius:CreateMinimapButton()
+ local GladiusLDB = LDB:NewDataObject("GladiusMinimapIcon", {
+ type = "data source",
+ icon = [[Interface\AddOns\Gladius\media\Icons\Gladius]],
+ text = "0",
+ OnClick = function(self, button)
+
+ if button == "LeftButton" then
+ Gladius:ShowOptions()
+ end
+ if button == "RightButton" then
+ Gladius:ToggleFrame(5)
+ end
+
+ end,
+ OnTooltipShow = function (tooltip)
+ tooltip:AddLine ("|cffe5e3e3Gladius|r |cff1784d1Enhanced|r")
+ tooltip:AddLine ("|cff1784d1Left Click|r: show/hide Gladius UI")
+ tooltip:AddLine ("|cff1784d1Right Click|r: show/hide Test Frames")
+ end,
+ })
+ LDBIcon:Register("GladiusMinimapIcon", GladiusLDB, Gladius.db.profile.minimapIcon)
+end
+
function Gladius:ZONE_CHANGED_NEW_AREA()
local type = select(2, IsInInstance())
diff --git a/Gladius/Gladius.toc b/Gladius/Gladius.toc
index 45fe78c..2bb02d1 100644
--- a/Gladius/Gladius.toc
+++ b/Gladius/Gladius.toc
@@ -4,7 +4,7 @@
## Version: v1.2.2
## Author: Proditor, Rinu, Enhanced by Cortes
## SavedVariables: GladiusDB
-## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibHealCommArena-4.0
+## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibDataBroker-1.1, LibDBIcon-1.0, LibHealCommArena-4.0
## LoadManagers: AddonLoader
## X-LoadOn-Slash: /gladius
## X-LoadOn-InterfaceOptions: Gladius
diff --git a/Gladius/embeds.xml b/Gladius/embeds.xml
index 8477ca5..270f514 100644
--- a/Gladius/embeds.xml
+++ b/Gladius/embeds.xml
@@ -1,6 +1,8 @@
+
+
diff --git a/Gladius/libs/LibDBIcon-1.0/LibDBIcon-1.0.lua b/Gladius/libs/LibDBIcon-1.0/LibDBIcon-1.0.lua
new file mode 100644
index 0000000..c5249f2
--- /dev/null
+++ b/Gladius/libs/LibDBIcon-1.0/LibDBIcon-1.0.lua
@@ -0,0 +1,264 @@
+--[[
+Name: DBIcon-1.0
+Revision: $Rev: 14 $
+Author(s): Rabbit (rabbit.magtheridon@gmail.com)
+Description: Allows addons to register to recieve a lightweight minimap icon as an alternative to more heavy LDB displays.
+Dependencies: LibStub
+License: GPL v2 or later.
+]]
+
+--[[
+Copyright (C) 2008-2010 Rabbit
+
+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, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+]]
+
+-----------------------------------------------------------------------
+-- DBIcon-1.0
+--
+-- Disclaimer: Most of this code was ripped from Barrel but fixed, streamlined
+-- and cleaned up a lot so that it no longer sucks.
+--
+
+local DBICON10 = "LibDBIcon-1.0"
+local DBICON10_MINOR = tonumber(("$Rev: 14 $"):match("(%d+)"))
+if not LibStub then error(DBICON10 .. " requires LibStub.") end
+local ldb = LibStub("LibDataBroker-1.1", true)
+if not ldb then error(DBICON10 .. " requires LibDataBroker-1.1.") end
+local lib = LibStub:NewLibrary(DBICON10, DBICON10_MINOR)
+if not lib then return end
+
+lib.disabled = lib.disabled or nil
+lib.objects = lib.objects or {}
+lib.callbackRegistered = lib.callbackRegistered or nil
+lib.notCreated = lib.notCreated or {}
+
+function lib:IconCallback(event, name, key, value, dataobj)
+ if lib.objects[name] then
+ lib.objects[name].icon:SetTexture(dataobj.icon)
+ end
+end
+if not lib.callbackRegistered then
+ ldb.RegisterCallback(lib, "LibDataBroker_AttributeChanged__icon", "IconCallback")
+ lib.callbackRegistered = true
+end
+
+-- Tooltip code ripped from StatBlockCore by Funkydude
+local function getAnchors(frame)
+ local x,y = frame:GetCenter()
+ if not x or not y then return "TOPLEFT", "BOTTOMLEFT" end
+ local hhalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or ""
+ local vhalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM"
+ return vhalf..hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP")..hhalf
+end
+
+local function onEnter(self)
+ if self.isMoving then return end
+ local obj = self.dataObject
+ if obj.OnTooltipShow then
+ GameTooltip:SetOwner(self, "ANCHOR_NONE")
+ GameTooltip:SetPoint(getAnchors(self))
+ obj.OnTooltipShow(GameTooltip)
+ GameTooltip:Show()
+ elseif obj.OnEnter then
+ obj.OnEnter(self)
+ end
+end
+
+local function onLeave(self)
+ local obj = self.dataObject
+ GameTooltip:Hide()
+ if obj.OnLeave then obj.OnLeave(self) end
+end
+
+--------------------------------------------------------------------------------
+
+local minimapShapes = {
+ ["ROUND"] = {true, true, true, true},
+ ["SQUARE"] = {false, false, false, false},
+ ["CORNER-TOPLEFT"] = {true, false, false, false},
+ ["CORNER-TOPRIGHT"] = {false, false, true, false},
+ ["CORNER-BOTTOMLEFT"] = {false, true, false, false},
+ ["CORNER-BOTTOMRIGHT"] = {false, false, false, true},
+ ["SIDE-LEFT"] = {true, true, false, false},
+ ["SIDE-RIGHT"] = {false, false, true, true},
+ ["SIDE-TOP"] = {true, false, true, false},
+ ["SIDE-BOTTOM"] = {false, true, false, true},
+ ["TRICORNER-TOPLEFT"] = {true, true, true, false},
+ ["TRICORNER-TOPRIGHT"] = {true, false, true, true},
+ ["TRICORNER-BOTTOMLEFT"] = {true, true, false, true},
+ ["TRICORNER-BOTTOMRIGHT"] = {false, true, true, true},
+}
+
+local function updatePosition(button)
+ local angle = math.rad(button.db.minimapPos or 225)
+ local x, y, q = math.cos(angle), math.sin(angle), 1
+ if x < 0 then q = q + 1 end
+ if y > 0 then q = q + 2 end
+ local minimapShape = GetMinimapShape and GetMinimapShape() or "ROUND"
+ local quadTable = minimapShapes[minimapShape]
+ if quadTable[q] then
+ x, y = x*80, y*80
+ else
+ local diagRadius = 103.13708498985 --math.sqrt(2*(80)^2)-10
+ x = math.max(-80, math.min(x*diagRadius, 80))
+ y = math.max(-80, math.min(y*diagRadius, 80))
+ end
+ button:SetPoint("CENTER", Minimap, "CENTER", x, y)
+end
+
+local function onClick(self, b) if self.dataObject.OnClick then self.dataObject.OnClick(self, b) end end
+local function onMouseDown(self) self.icon:SetTexCoord(0, 1, 0, 1) end
+local function onMouseUp(self) self.icon:SetTexCoord(0.05, 0.95, 0.05, 0.95) end
+
+local function onUpdate(self)
+ local mx, my = Minimap:GetCenter()
+ local px, py = GetCursorPosition()
+ local scale = Minimap:GetEffectiveScale()
+ px, py = px / scale, py / scale
+ self.db.minimapPos = math.deg(math.atan2(py - my, px - mx)) % 360
+ updatePosition(self)
+end
+
+local function onDragStart(self)
+ self:LockHighlight()
+ self.icon:SetTexCoord(0, 1, 0, 1)
+ self:SetScript("OnUpdate", onUpdate)
+ self.isMoving = true
+ GameTooltip:Hide()
+end
+
+local function onDragStop(self)
+ self:SetScript("OnUpdate", nil)
+ self.icon:SetTexCoord(0.05, 0.95, 0.05, 0.95)
+ self:UnlockHighlight()
+ self.isMoving = nil
+end
+
+local function createButton(name, object, db)
+ local button = CreateFrame("Button", "LibDBIcon10_"..name, Minimap)
+ button.dataObject = object
+ button.db = db
+ button:SetFrameStrata("MEDIUM")
+ button:SetWidth(31); button:SetHeight(31)
+ button:SetFrameLevel(8)
+ button:RegisterForClicks("anyUp")
+ button:RegisterForDrag("LeftButton")
+ button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
+ local overlay = button:CreateTexture(nil, "OVERLAY")
+ overlay:SetWidth(53); overlay:SetHeight(53)
+ overlay:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
+ overlay:SetPoint("TOPLEFT")
+ local icon = button:CreateTexture(nil, "BACKGROUND")
+ icon:SetWidth(20); icon:SetHeight(20)
+ icon:SetTexture(object.icon)
+ icon:SetTexCoord(0.05, 0.95, 0.05, 0.95)
+ icon:SetPoint("TOPLEFT", 7, -5)
+ button.icon = icon
+
+ button:SetScript("OnEnter", onEnter)
+ button:SetScript("OnLeave", onLeave)
+ button:SetScript("OnClick", onClick)
+ button:SetScript("OnDragStart", onDragStart)
+ button:SetScript("OnDragStop", onDragStop)
+ button:SetScript("OnMouseDown", onMouseDown)
+ button:SetScript("OnMouseUp", onMouseUp)
+
+ lib.objects[name] = button
+
+ if lib.loggedIn then
+ updatePosition(button)
+ if not db.hide then button:Show()
+ else button:Hide() end
+ end
+end
+
+-- We could use a metatable.__index on lib.objects, but then we'd create
+-- the icons when checking things like :IsRegistered, which is not necessary.
+local function check(name)
+ if lib.notCreated[name] then
+ createButton(name, lib.notCreated[name][1], lib.notCreated[name][2])
+ lib.notCreated[name] = nil
+ end
+end
+
+lib.loggedIn = lib.loggedIn or false
+-- Wait a bit with the initial positioning to let any GetMinimapShape addons
+-- load up.
+if not lib.loggedIn then
+ local f = CreateFrame("Frame")
+ f:SetScript("OnEvent", function()
+ for _, object in pairs(lib.objects) do
+ updatePosition(object)
+ if not lib.disabled and not object.db.hide then object:Show()
+ else object:Hide() end
+ end
+ lib.loggedIn = true
+ f:SetScript("OnEvent", nil)
+ f = nil
+ end)
+ f:RegisterEvent("PLAYER_LOGIN")
+end
+
+function lib:Register(name, object, db)
+ if lib.disabled then return end
+ if not object.icon then error("Can't register LDB objects without icons set!") end
+ if lib.objects[name] or lib.notCreated[name] then error("Already registered, nubcake.") end
+ if not db or not db.hide then
+ createButton(name, object, db)
+ else
+ lib.notCreated[name] = {object, db}
+ end
+end
+
+function lib:Hide(name)
+ if not lib.objects[name] then return end
+ lib.objects[name]:Hide()
+end
+function lib:Show(name)
+ if lib.disabled then return end
+ check(name)
+ lib.objects[name]:Show()
+ updatePosition(lib.objects[name])
+end
+function lib:IsRegistered(name)
+ return (lib.objects[name] or lib.notCreated[name]) and true or false
+end
+function lib:Refresh(name, db)
+ if lib.disabled then return end
+ check(name)
+ local button = lib.objects[name]
+ if db then button.db = db end
+ updatePosition(button)
+ if not db.hide then button:Show() else button:Hide() end
+end
+
+function lib:EnableLibrary()
+ lib.disabled = nil
+ for name, object in pairs(lib.objects) do
+ if not object.db or (object.db and not object.db.hide) then
+ object:Show()
+ updatePosition(object)
+ end
+ end
+end
+
+function lib:DisableLibrary()
+ lib.disabled = true
+ for name, object in pairs(lib.objects) do
+ object:Hide()
+ end
+end
+
diff --git a/Gladius/libs/LibDataBroker-1.1/LibDataBroker-1.1.lua b/Gladius/libs/LibDataBroker-1.1/LibDataBroker-1.1.lua
new file mode 100644
index 0000000..f47c0cd
--- /dev/null
+++ b/Gladius/libs/LibDataBroker-1.1/LibDataBroker-1.1.lua
@@ -0,0 +1,90 @@
+
+assert(LibStub, "LibDataBroker-1.1 requires LibStub")
+assert(LibStub:GetLibrary("CallbackHandler-1.0", true), "LibDataBroker-1.1 requires CallbackHandler-1.0")
+
+local lib, oldminor = LibStub:NewLibrary("LibDataBroker-1.1", 4)
+if not lib then return end
+oldminor = oldminor or 0
+
+
+lib.callbacks = lib.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(lib)
+lib.attributestorage, lib.namestorage, lib.proxystorage = lib.attributestorage or {}, lib.namestorage or {}, lib.proxystorage or {}
+local attributestorage, namestorage, callbacks = lib.attributestorage, lib.namestorage, lib.callbacks
+
+if oldminor < 2 then
+ lib.domt = {
+ __metatable = "access denied",
+ __index = function(self, key) return attributestorage[self] and attributestorage[self][key] end,
+ }
+end
+
+if oldminor < 3 then
+ lib.domt.__newindex = function(self, key, value)
+ if not attributestorage[self] then attributestorage[self] = {} end
+ if attributestorage[self][key] == value then return end
+ attributestorage[self][key] = value
+ local name = namestorage[self]
+ if not name then return end
+ callbacks:Fire("LibDataBroker_AttributeChanged", name, key, value, self)
+ callbacks:Fire("LibDataBroker_AttributeChanged_"..name, name, key, value, self)
+ callbacks:Fire("LibDataBroker_AttributeChanged_"..name.."_"..key, name, key, value, self)
+ callbacks:Fire("LibDataBroker_AttributeChanged__"..key, name, key, value, self)
+ end
+end
+
+if oldminor < 2 then
+ function lib:NewDataObject(name, dataobj)
+ if self.proxystorage[name] then return end
+
+ if dataobj then
+ assert(type(dataobj) == "table", "Invalid dataobj, must be nil or a table")
+ self.attributestorage[dataobj] = {}
+ for i,v in pairs(dataobj) do
+ self.attributestorage[dataobj][i] = v
+ dataobj[i] = nil
+ end
+ end
+ dataobj = setmetatable(dataobj or {}, self.domt)
+ self.proxystorage[name], self.namestorage[dataobj] = dataobj, name
+ self.callbacks:Fire("LibDataBroker_DataObjectCreated", name, dataobj)
+ return dataobj
+ end
+end
+
+if oldminor < 1 then
+ function lib:DataObjectIterator()
+ return pairs(self.proxystorage)
+ end
+
+ function lib:GetDataObjectByName(dataobjectname)
+ return self.proxystorage[dataobjectname]
+ end
+
+ function lib:GetNameByDataObject(dataobject)
+ return self.namestorage[dataobject]
+ end
+end
+
+if oldminor < 4 then
+ local next = pairs(attributestorage)
+ function lib:pairs(dataobject_or_name)
+ local t = type(dataobject_or_name)
+ assert(t == "string" or t == "table", "Usage: ldb:pairs('dataobjectname') or ldb:pairs(dataobject)")
+
+ local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name
+ assert(attributestorage[dataobj], "Data object not found")
+
+ return next, attributestorage[dataobj], nil
+ end
+
+ local ipairs_iter = ipairs(attributestorage)
+ function lib:ipairs(dataobject_or_name)
+ local t = type(dataobject_or_name)
+ assert(t == "string" or t == "table", "Usage: ldb:ipairs('dataobjectname') or ldb:ipairs(dataobject)")
+
+ local dataobj = self.proxystorage[dataobject_or_name] or dataobject_or_name
+ assert(attributestorage[dataobj], "Data object not found")
+
+ return ipairs_iter, attributestorage[dataobj], 0
+ end
+end
diff --git a/Gladius/libs/LibDataBroker-1.1/README.textile b/Gladius/libs/LibDataBroker-1.1/README.textile
new file mode 100644
index 0000000..ef16fed
--- /dev/null
+++ b/Gladius/libs/LibDataBroker-1.1/README.textile
@@ -0,0 +1,13 @@
+LibDataBroker is a small WoW addon library designed to provide a "MVC":http://en.wikipedia.org/wiki/Model-view-controller interface for use in various addons.
+LDB's primary goal is to "detach" plugins for TitanPanel and FuBar from the display addon.
+Plugins can provide data into a simple table, and display addons can receive callbacks to refresh their display of this data.
+LDB also provides a place for addons to register "quicklaunch" functions, removing the need for authors to embed many large libraries to create minimap buttons.
+Users who do not wish to be "plagued" by these buttons simply do not install an addon to render them.
+
+Due to it's simple generic design, LDB can be used for any design where you wish to have an addon notified of changes to a table.
+
+h2. Links
+
+* "API documentation":http://github.com/tekkub/libdatabroker-1-1/wikis/api
+* "Data specifications":http://github.com/tekkub/libdatabroker-1-1/wikis/data-specifications
+* "Addons using LDB":http://github.com/tekkub/libdatabroker-1-1/wikis/addons-using-ldb
diff --git a/Gladius/media/Icons/Gladius.blp b/Gladius/media/Icons/Gladius.blp
new file mode 100644
index 0000000000000000000000000000000000000000..6b4ec9dc1e4772aaf0576f5089b20bc176656105
GIT binary patch
literal 3916
zcmeHJZD?C%6h8gJn{{a~XfsFgJ;k>TN~PP36jdOoyeH
zfQ9*MVuSuzLx~@okxh_RL@W@f1@p;UvqwhZU8r^JwSd!KF|ILf1PRIciO)?Y~zc5f!7+m
z|0j-1#3S%;Cj;Ex?aUYUh247f{PQ-xup{Eo97DoSH=gk4AhcWjKV1cE+yF2@`NP8g
ziaIltpzkg!!|)(H0;4bjA$SymJk*ktoi|Q1U%hyz#hdS!FD_TF`{m#EeDD{=FXT1t
z<#W~bxgYjh{l-0?&aj>PZtk&o6yuF!FPz|Z_T_q__a@iaYhHEi#q4zybLM`0*z!-*
z|5&l}w3ixUR6e`)rT44-68Zb(F`IA0vG>Z+>iiSdZ2y=;!Lyh@e8L|8;&*HJ#U6XB
z0c}@Dj<1PB*b%)#yVuofzq()2iWpay-fDhTf7?75ycYg+o26q;-UA@2B>(Hpc|erH
zM4yH@F2Ty<>TRWQX)dO!yPu?16>(rGDeod45hrBmDX1k(%5*S$cgkzdKGxDM@o6R2P@hLSGps3FB{ZOwTh?gFtR99vzgd4qv`@KITk^SYOnUqzv
z(8@RzHT+p*9V32NGj3A$Yy(&LnKQ>FO&^VOJZA{7o7hL3x`X*$@HVZB2Qj{6Sgq%d
z66x08PV#!Pl93aUNL27VMm%)%x9oU5MSNT7<%<(?ltkc9Rsg&nkHGcClw9shcNY(Eg2
zyXwh5x1H_Q$0~>^>t2w;)^S+l{@X6ze?1Rz(FcZ}Sf>5DLig1}M;3iO6#rOd(bpJI
zQ0B8{8bk`#8z{cdYq$5$^mNwUjO_mniI07>w0II
z?!AuwtLIcmN}H<);i))
zncvRt@q9i3=+T^ch;%=_W7uP8Eb+T1uP8b5Engwv_2kUcgSP-grErmWRU;+r4l8*e
zUX!wy?&U<8;?xucV6qY+S&uUvq~RoKGeQfUHs1|`_O*}K8N9ppBbEn!e(s$P_6_w?
ZydTbV-di*h<$
Date: Mon, 3 Jan 2022 10:40:51 +0100
Subject: [PATCH 3/3] LUA error 'AbsorbMonitor' fixed
https://github.com/Cortes-Jeremy/Gladius/issues/10
---
Gladius/Gladius.lua | 40 +-
Gladius/Gladius.toc | 2 +-
Gladius/embeds.xml | 6 +-
Gladius/frame.lua | 2 +-
Gladius/libs/AceComm-3.0/AceComm-3.0.lua | 309 +++
Gladius/libs/AceComm-3.0/AceComm-3.0.xml | 5 +
Gladius/libs/AceComm-3.0/ChatThrottleLib.lua | 503 ++++
.../AceSerializer-3.0/AceSerializer-3.0.lua | 281 ++
.../AceSerializer-3.0/AceSerializer-3.0.xml | 4 +
Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua | 473 ++++
Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml | 4 +
.../SpecializedAbsorbs-1.0.lua | 2352 +++++++++++++++++
.../SpecializedAbsorbs-1.0.xml | 4 +
13 files changed, 3950 insertions(+), 35 deletions(-)
create mode 100644 Gladius/libs/AceComm-3.0/AceComm-3.0.lua
create mode 100644 Gladius/libs/AceComm-3.0/AceComm-3.0.xml
create mode 100644 Gladius/libs/AceComm-3.0/ChatThrottleLib.lua
create mode 100644 Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua
create mode 100644 Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml
create mode 100644 Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua
create mode 100644 Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml
create mode 100644 Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua
create mode 100644 Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml
diff --git a/Gladius/Gladius.lua b/Gladius/Gladius.lua
index 2d3c5d8..1df94ea 100644
--- a/Gladius/Gladius.lua
+++ b/Gladius/Gladius.lua
@@ -1,8 +1,8 @@
-Gladius = LibStub("AceAddon-3.0"):NewAddon("Gladius", "AceEvent-3.0", "AceConsole-3.0")
+Gladius = LibStub("AceAddon-3.0"):NewAddon("Gladius", "AceEvent-3.0", "AceConsole-3.0", "AceComm-3.0", "AceTimer-3.0", "AceSerializer-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("Gladius", true)
local LSM = LibStub("LibSharedMedia-3.0")
local LCG = LibStub("LibCustomGlow-1.0")
-local LHC = LibStub("LibHealCommArena-4.0")
+--[[ local LHC = LibStub("LibHealCommArena-4.0") ]]
local LAM = LibStub:GetLibrary("AbsorbsMonitor-1.0", true)
local LDB = LibStub ("LibDataBroker-1.1")
local LDBIcon = LibStub("LibDBIcon-1.0")
@@ -27,13 +27,13 @@ function Gladius:OnInitialize()
self.buttons = {}
self.currentBracket = nil
- LHC.UnregisterAllCallbacks(Gladius);
+ --[[ LHC.UnregisterAllCallbacks(Gladius);
LHC.RegisterCallback(Gladius, "HealCommArena_HealStarted", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_HealUpdated", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_HealDelayed", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_HealStopped", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_ModifierChanged", "HealCommArena_Modified")
- LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified")
+ LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified") ]]
-- Populate the arenaUnits table
for i=1, 5 do
@@ -270,7 +270,7 @@ function Gladius:JoinedArena()
LHC.RegisterCallback(Gladius, "HealCommArena_ModifierChanged", "HealCommArena_Modified")
LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified")]]
- -- HealComm Events
+ -- Absorb Events
LAM.UnregisterAllCallbacks(Gladius);
LAM.RegisterCallback(Gladius, "EffectApplied");
LAM.RegisterCallback(Gladius, "EffectUpdated");
@@ -1825,7 +1825,7 @@ function Gladius:UpdateAbsorbBar(event, unit, button)
local health = UnitHealth(unit)
local maxHealth = UnitHealthMax(unit)
local _guid = UnitGUID(unit)
- local myCurrentHealAbsorb = LAM.Unit_Total(_guid)
+ local myCurrentHealAbsorb = LAM.Unit_Total(_guid) * 2 -- only enemy
--
function CompactUnitFrameUtil_UpdateFillBar(self, frame, previousTexture, health, myCurrentHealAbsorb)
@@ -1867,16 +1867,10 @@ function Gladius:UpdateAbsorbBar(event, unit, button)
end
-- HealComm (Heal Prediction)
-local function Update(self)
+--[[ local function Update(self)
local unit = self.unit
local element = self.HealCommBar
- --[[ Callback: HealthPrediction:PreUpdate(unit)
- Called before the element has been updated.
-
- * self - the HealthPrediction element
- * unit - the unit for which the update has been triggered (string)
- --]]
if element.PreUpdate then
element:PreUpdate(unit)
end
@@ -1912,27 +1906,9 @@ local function Update(self)
element.otherBar:Show()
end
- --[[ Callback: HealthPrediction:PostUpdate(unit, myIncomingHeal, otherIncomingHeal)
- Called after the element has been updated.
-
- * self - the HealthPrediction element
- * unit - the unit for which the update has been triggered (string)
- * myIncomingHeal - the amount of incoming healing done by the player (number)
- * otherIncomingHeal - the amount of incoming healing done by others (number)
- --]]
- if element.PostUpdate then
- return element:PostUpdate(unit, myIncomingHeal, otherIncomingHeal)
- end
end
local function MultiUpdate(...)
for i = 1, select("#", ...) do
- --[[ for j = 1, #enabledUF do
- local frame = enabledUF[j]
-
- if frame.unit and frame:IsVisible() and UnitGUID(frame.unit) == select(i, ...) then
- Path(frame)
- end
- end ]]
if (UnitGUID('arena1') == select(i, ...)) then
local allIncomingHeal = LHC:GetHealAmount(select(i, ...), LHC.ALL_HEALS, nil) or 0
print('updated arena1', allIncomingHeal)
@@ -1947,4 +1923,4 @@ function Gladius:HealCommArena_Heal_Update(event, casterGUID, spellID, spellType
end
function Gladius:HealCommArena_Modified(event, guid)
MultiUpdate(guid)
-end
+end ]]
diff --git a/Gladius/Gladius.toc b/Gladius/Gladius.toc
index 2bb02d1..3b15139 100644
--- a/Gladius/Gladius.toc
+++ b/Gladius/Gladius.toc
@@ -4,7 +4,7 @@
## Version: v1.2.2
## Author: Proditor, Rinu, Enhanced by Cortes
## SavedVariables: GladiusDB
-## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibDataBroker-1.1, LibDBIcon-1.0, LibHealCommArena-4.0
+## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibDataBroker-1.1, LibDBIcon-1.0
## LoadManagers: AddonLoader
## X-LoadOn-Slash: /gladius
## X-LoadOn-InterfaceOptions: Gladius
diff --git a/Gladius/embeds.xml b/Gladius/embeds.xml
index 270f514..83c717a 100644
--- a/Gladius/embeds.xml
+++ b/Gladius/embeds.xml
@@ -12,10 +12,14 @@
+
+
+
-
+
+
\ No newline at end of file
diff --git a/Gladius/frame.lua b/Gladius/frame.lua
index 582e78b..c324ef3 100644
--- a/Gladius/frame.lua
+++ b/Gladius/frame.lua
@@ -273,7 +273,7 @@ function Gladius:CreateButton(i)
absorbBar.overAbsorbGlow:SetBlendMode("ADD");
absorbBar.overAbsorbGlow:Hide()
-- Total absorb
- absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BORDER")
+ absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BACKGROUND")
absorbBar.totalAbsorb:Hide()
-- Total absorb overlay
absorbBar.totalAbsorbOverlay = absorbBar:CreateTexture(nil, "BORDER")
diff --git a/Gladius/libs/AceComm-3.0/AceComm-3.0.lua b/Gladius/libs/AceComm-3.0/AceComm-3.0.lua
new file mode 100644
index 0000000..ad5268f
--- /dev/null
+++ b/Gladius/libs/AceComm-3.0/AceComm-3.0.lua
@@ -0,0 +1,309 @@
+--- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels.
+-- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\
+-- **ChatThrottleLib** is of course being used to avoid being disconnected by the server.
+--
+-- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by
+-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
+-- and can be accessed directly, without having to explicitly call AceComm itself.\\
+-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
+-- make into AceComm.
+-- @class file
+-- @name AceComm-3.0
+-- @release $Id: AceComm-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
+
+--[[ AceComm-3.0
+
+TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited.
+
+]]
+
+local MAJOR, MINOR = "AceComm-3.0", 6
+
+local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceComm then return end
+
+local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
+local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
+
+-- Lua APIs
+local type, next, pairs, tostring = type, next, pairs, tostring
+local strsub, strfind = string.sub, string.find
+local tinsert, tconcat = table.insert, table.concat
+local error, assert = error, assert
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler
+
+AceComm.embeds = AceComm.embeds or {}
+
+-- for my sanity and yours, let's give the message type bytes some names
+local MSG_MULTI_FIRST = "\001"
+local MSG_MULTI_NEXT = "\002"
+local MSG_MULTI_LAST = "\003"
+
+AceComm.multipart_origprefixes = AceComm.multipart_origprefixes or {} -- e.g. "Prefix\001"="Prefix", "Prefix\002"="Prefix"
+AceComm.multipart_reassemblers = AceComm.multipart_reassemblers or {} -- e.g. "Prefix\001"="OnReceiveMultipartFirst"
+
+-- the multipart message spool: indexed by a combination of sender+distribution+
+AceComm.multipart_spool = AceComm.multipart_spool or {}
+
+--- Register for Addon Traffic on a specified prefix
+-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
+-- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived"
+function AceComm:RegisterComm(prefix, method)
+ if method == nil then
+ method = "OnCommReceived"
+ end
+
+ return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
+end
+
+local warnedPrefix=false
+
+--- Send a message over the Addon Channel
+-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
+-- @param text Data to send, nils (\000) not allowed. Any length.
+-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
+-- @param target Destination for some distributions; see SendAddonMessage API
+-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
+-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send.
+-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
+function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
+ prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery!
+ if not( type(prefix)=="string" and
+ type(text)=="string" and
+ type(distribution)=="string" and
+ (target==nil or type(target)=="string") and
+ (prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
+ ) then
+ error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
+ end
+
+ if strfind(prefix, "[\001-\009]") then
+ if strfind(prefix, "[\001-\003]") then
+ error("SendCommMessage: Characters \\001--\\003 in prefix are reserved for AceComm metadata", 2)
+ elseif not warnedPrefix then
+ -- I have some ideas about future extensions that require more control characters /mikk, 20090808
+ geterrorhandler()("SendCommMessage: Heads-up developers: Characters \\004--\\009 in prefix are reserved for AceComm future extension")
+ warnedPrefix = true
+ end
+ end
+
+
+ local textlen = #text
+ local maxtextlen = 254 - #prefix -- 254 is the max length of prefix + text that can be sent in one message
+ local queueName = prefix..distribution..(target or "")
+
+ local ctlCallback = nil
+ if callbackFn then
+ ctlCallback = function(sent)
+ return callbackFn(callbackArg, sent, textlen)
+ end
+ end
+
+ if textlen <= maxtextlen then
+ -- fits all in one message
+ CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
+ else
+ maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix
+
+ -- first part
+ local chunk = strsub(text, 1, maxtextlen)
+ CTL:SendAddonMessage(prio, prefix..MSG_MULTI_FIRST, chunk, distribution, target, queueName, ctlCallback, maxtextlen)
+
+ -- continuation
+ local pos = 1+maxtextlen
+ local prefix2 = prefix..MSG_MULTI_NEXT
+
+ while pos+maxtextlen <= textlen do
+ chunk = strsub(text, pos, pos+maxtextlen-1)
+ CTL:SendAddonMessage(prio, prefix2, chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
+ pos = pos + maxtextlen
+ end
+
+ -- final part
+ chunk = strsub(text, pos)
+ CTL:SendAddonMessage(prio, prefix..MSG_MULTI_LAST, chunk, distribution, target, queueName, ctlCallback, textlen)
+ end
+end
+
+
+----------------------------------------
+-- Message receiving
+----------------------------------------
+
+do
+ local compost = setmetatable({}, {__mode = "k"})
+ local function new()
+ local t = next(compost)
+ if t then
+ compost[t]=nil
+ for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten
+ t[i]=nil
+ end
+ return t
+ end
+
+ return {}
+ end
+
+ local function lostdatawarning(prefix,sender,where)
+ DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
+ end
+
+ function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender)
+ local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
+ local spool = AceComm.multipart_spool
+
+ --[[
+ if spool[key] then
+ lostdatawarning(prefix,sender,"First")
+ -- continue and overwrite
+ end
+ --]]
+
+ spool[key] = message -- plain string for now
+ end
+
+ function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender)
+ local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
+ local spool = AceComm.multipart_spool
+ local olddata = spool[key]
+
+ if not olddata then
+ --lostdatawarning(prefix,sender,"Next")
+ return
+ end
+
+ if type(olddata)~="table" then
+ -- ... but what we have is not a table. So make it one. (Pull a composted one if available)
+ local t = new()
+ t[1] = olddata -- add old data as first string
+ t[2] = message -- and new message as second string
+ spool[key] = t -- and put the table in the spool instead of the old string
+ else
+ tinsert(olddata, message)
+ end
+ end
+
+ function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender)
+ local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
+ local spool = AceComm.multipart_spool
+ local olddata = spool[key]
+
+ if not olddata then
+ --lostdatawarning(prefix,sender,"End")
+ return
+ end
+
+ spool[key] = nil
+
+ if type(olddata) == "table" then
+ -- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
+ tinsert(olddata, message)
+ AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
+ compost[olddata] = true
+ else
+ -- if we've only received a "first", the spooled data will still only be a string
+ AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
+ end
+ end
+end
+
+
+
+
+
+
+----------------------------------------
+-- Embed CallbackHandler
+----------------------------------------
+
+if not AceComm.callbacks then
+ -- ensure that 'prefix to watch' table is consistent with registered
+ -- callbacks
+ AceComm.__prefixes = {}
+
+ AceComm.callbacks = CallbackHandler:New(AceComm,
+ "_RegisterComm",
+ "UnregisterComm",
+ "UnregisterAllComm")
+end
+
+function AceComm.callbacks:OnUsed(target, prefix)
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_FIRST] = prefix
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_FIRST] = "OnReceiveMultipartFirst"
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_NEXT] = prefix
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_NEXT] = "OnReceiveMultipartNext"
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_LAST] = prefix
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_LAST] = "OnReceiveMultipartLast"
+end
+
+function AceComm.callbacks:OnUnused(target, prefix)
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_FIRST] = nil
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_FIRST] = nil
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_NEXT] = nil
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_NEXT] = nil
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_LAST] = nil
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_LAST] = nil
+end
+
+local function OnEvent(this, event, ...)
+ if event == "CHAT_MSG_ADDON" then
+ local prefix,message,distribution,sender = ...
+ local reassemblername = AceComm.multipart_reassemblers[prefix]
+ if reassemblername then
+ -- multipart: reassemble
+ local aceCommReassemblerFunc = AceComm[reassemblername]
+ local origprefix = AceComm.multipart_origprefixes[prefix]
+ aceCommReassemblerFunc(AceComm, origprefix, message, distribution, sender)
+ else
+ -- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
+ AceComm.callbacks:Fire(prefix, message, distribution, sender)
+ end
+ else
+ assert(false, "Received "..tostring(event).." event?!")
+ end
+end
+
+AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
+AceComm.frame:SetScript("OnEvent", OnEvent)
+AceComm.frame:UnregisterAllEvents()
+AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")
+
+
+----------------------------------------
+-- Base library stuff
+----------------------------------------
+
+local mixins = {
+ "RegisterComm",
+ "UnregisterComm",
+ "UnregisterAllComm",
+ "SendCommMessage",
+}
+
+-- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:..
+-- @param target target object to embed AceComm-3.0 in
+function AceComm:Embed(target)
+ for k, v in pairs(mixins) do
+ target[v] = self[v]
+ end
+ self.embeds[target] = true
+ return target
+end
+
+function AceComm:OnEmbedDisable(target)
+ target:UnregisterAllComm()
+end
+
+-- Update embeds
+for target, v in pairs(AceComm.embeds) do
+ AceComm:Embed(target)
+end
diff --git a/Gladius/libs/AceComm-3.0/AceComm-3.0.xml b/Gladius/libs/AceComm-3.0/AceComm-3.0.xml
new file mode 100644
index 0000000..09e8d87
--- /dev/null
+++ b/Gladius/libs/AceComm-3.0/AceComm-3.0.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/AceComm-3.0/ChatThrottleLib.lua b/Gladius/libs/AceComm-3.0/ChatThrottleLib.lua
new file mode 100644
index 0000000..b0afc92
--- /dev/null
+++ b/Gladius/libs/AceComm-3.0/ChatThrottleLib.lua
@@ -0,0 +1,503 @@
+--
+-- ChatThrottleLib by Mikk
+--
+-- Manages AddOn chat output to keep player from getting kicked off.
+--
+-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
+-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
+--
+-- Priorities get an equal share of available bandwidth when fully loaded.
+-- Communication channels are separated on extension+chattype+destination and
+-- get round-robinned. (Destination only matters for whispers and channels,
+-- obviously)
+--
+-- Will install hooks for SendChatMessage and SendAddonMessage to measure
+-- bandwidth bypassing the library and use less bandwidth itself.
+--
+--
+-- Fully embeddable library. Just copy this file into your addon directory,
+-- add it to the .toc, and it's done.
+--
+-- Can run as a standalone addon also, but, really, just embed it! :-)
+--
+
+local CTL_VERSION = 21
+
+local _G = _G
+
+if _G.ChatThrottleLib then
+ if _G.ChatThrottleLib.version >= CTL_VERSION then
+ -- There's already a newer (or same) version loaded. Buh-bye.
+ return
+ elseif not _G.ChatThrottleLib.securelyHooked then
+ print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, =v16) in it!")
+ -- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
+ -- ... and if someone has securehooked, they can kiss that goodbye too... >.<
+ _G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
+ if _G.ChatThrottleLib.ORIG_SendAddonMessage then
+ _G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
+ end
+ end
+ _G.ChatThrottleLib.ORIG_SendChatMessage = nil
+ _G.ChatThrottleLib.ORIG_SendAddonMessage = nil
+end
+
+if not _G.ChatThrottleLib then
+ _G.ChatThrottleLib = {}
+end
+
+ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
+local ChatThrottleLib = _G.ChatThrottleLib
+
+ChatThrottleLib.version = CTL_VERSION
+
+
+
+------------------ TWEAKABLES -----------------
+
+ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
+ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
+
+ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
+
+ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
+
+
+local setmetatable = setmetatable
+local table_remove = table.remove
+local tostring = tostring
+local GetTime = GetTime
+local math_min = math.min
+local math_max = math.max
+local next = next
+local strlen = string.len
+local GetFrameRate = GetFrameRate
+
+
+
+-----------------------------------------------------------------------
+-- Double-linked ring implementation
+
+local Ring = {}
+local RingMeta = { __index = Ring }
+
+function Ring:New()
+ local ret = {}
+ setmetatable(ret, RingMeta)
+ return ret
+end
+
+function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
+ if self.pos then
+ obj.prev = self.pos.prev
+ obj.prev.next = obj
+ obj.next = self.pos
+ obj.next.prev = obj
+ else
+ obj.next = obj
+ obj.prev = obj
+ self.pos = obj
+ end
+end
+
+function Ring:Remove(obj)
+ obj.next.prev = obj.prev
+ obj.prev.next = obj.next
+ if self.pos == obj then
+ self.pos = obj.next
+ if self.pos == obj then
+ self.pos = nil
+ end
+ end
+end
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for pipes
+-- A pipe is a plain integer-indexed queue, which also happens to be a ring member
+
+ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
+local PipeBin = setmetatable({}, {__mode="k"})
+
+local function DelPipe(pipe)
+ for i = #pipe, 1, -1 do
+ pipe[i] = nil
+ end
+ pipe.prev = nil
+ pipe.next = nil
+
+ PipeBin[pipe] = true
+end
+
+local function NewPipe()
+ local pipe = next(PipeBin)
+ if pipe then
+ PipeBin[pipe] = nil
+ return pipe
+ end
+ return {}
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Recycling bin for messages
+
+ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
+local MsgBin = setmetatable({}, {__mode="k"})
+
+local function DelMsg(msg)
+ msg[1] = nil
+ -- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
+ MsgBin[msg] = true
+end
+
+local function NewMsg()
+ local msg = next(MsgBin)
+ if msg then
+ MsgBin[msg] = nil
+ return msg
+ end
+ return {}
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:Init
+-- Initialize queues, set up frame for OnUpdate, etc
+
+
+function ChatThrottleLib:Init()
+
+ -- Set up queues
+ if not self.Prio then
+ self.Prio = {}
+ self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
+ end
+
+ -- v4: total send counters per priority
+ for _, Prio in pairs(self.Prio) do
+ Prio.nTotalSent = Prio.nTotalSent or 0
+ end
+
+ if not self.avail then
+ self.avail = 0 -- v5
+ end
+ if not self.nTotalSent then
+ self.nTotalSent = 0 -- v5
+ end
+
+
+ -- Set up a frame to get OnUpdate events
+ if not self.Frame then
+ self.Frame = CreateFrame("Frame")
+ self.Frame:Hide()
+ end
+ self.Frame:SetScript("OnUpdate", self.OnUpdate)
+ self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
+ self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
+ self.OnUpdateDelay = 0
+ self.LastAvailUpdate = GetTime()
+ self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
+
+ -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
+ if not self.securelyHooked then
+ -- Use secure hooks as of v16. Old regular hook support yanked out in v21.
+ self.securelyHooked = true
+ --SendChatMessage
+ hooksecurefunc("SendChatMessage", function(...)
+ return ChatThrottleLib.Hook_SendChatMessage(...)
+ end)
+ --SendAddonMessage
+ hooksecurefunc("SendAddonMessage", function(...)
+ return ChatThrottleLib.Hook_SendAddonMessage(...)
+ end)
+ end
+ self.nBypass = 0
+end
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
+
+local bMyTraffic = false
+
+function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
+ if bMyTraffic then
+ return
+ end
+ local self = ChatThrottleLib
+ local size = tostring(text or ""):len() + tostring(prefix or ""):len();
+ size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
+ self.avail = self.avail - size
+ self.nBypass = self.nBypass + size -- just a statistic
+end
+
+
+
+-----------------------------------------------------------------------
+-- ChatThrottleLib:UpdateAvail
+-- Update self.avail with how much bandwidth is currently available
+
+function ChatThrottleLib:UpdateAvail()
+ local now = GetTime()
+ local MAX_CPS = self.MAX_CPS;
+ local newavail = MAX_CPS * (now - self.LastAvailUpdate)
+ local avail = self.avail
+
+ if now - self.HardThrottlingBeginTime < 5 then
+ -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
+ avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
+ self.bChoking = true
+ elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs
+ avail = math_min(MAX_CPS, avail + newavail*0.5)
+ self.bChoking = true -- just a statistic
+ else
+ avail = math_min(self.BURST, avail + newavail)
+ self.bChoking = false
+ end
+
+ avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
+
+ self.avail = avail
+ self.LastAvailUpdate = now
+
+ return avail
+end
+
+
+-----------------------------------------------------------------------
+-- Despooling logic
+
+function ChatThrottleLib:Despool(Prio)
+ local ring = Prio.Ring
+ while ring.pos and Prio.avail > ring.pos[1].nSize do
+ local msg = table_remove(Prio.Ring.pos, 1)
+ if not Prio.Ring.pos[1] then
+ local pipe = Prio.Ring.pos
+ Prio.Ring:Remove(pipe)
+ Prio.ByName[pipe.name] = nil
+ DelPipe(pipe)
+ else
+ Prio.Ring.pos = Prio.Ring.pos.next
+ end
+ Prio.avail = Prio.avail - msg.nSize
+ bMyTraffic = true
+ msg.f(unpack(msg, 1, msg.n))
+ bMyTraffic = false
+ Prio.nTotalSent = Prio.nTotalSent + msg.nSize
+ DelMsg(msg)
+ if msg.callbackFn then
+ msg.callbackFn (msg.callbackArg)
+ end
+ end
+end
+
+
+function ChatThrottleLib.OnEvent(this,event)
+ -- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
+ local self = ChatThrottleLib
+ if event == "PLAYER_ENTERING_WORLD" then
+ self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
+ self.avail = 0
+ end
+end
+
+
+function ChatThrottleLib.OnUpdate(this,delay)
+ local self = ChatThrottleLib
+
+ self.OnUpdateDelay = self.OnUpdateDelay + delay
+ if self.OnUpdateDelay < 0.08 then
+ return
+ end
+ self.OnUpdateDelay = 0
+
+ self:UpdateAvail()
+
+ if self.avail < 0 then
+ return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
+ end
+
+ -- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
+ local n = 0
+ for prioname,Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ n = n + 1
+ end
+ end
+
+ -- Anything queued still?
+ if n<1 then
+ -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
+ for prioname, Prio in pairs(self.Prio) do
+ self.avail = self.avail + Prio.avail
+ Prio.avail = 0
+ end
+ self.bQueueing = false
+ self.Frame:Hide()
+ return
+ end
+
+ -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
+ local avail = self.avail/n
+ self.avail = 0
+
+ for prioname, Prio in pairs(self.Prio) do
+ if Prio.Ring.pos or Prio.avail < 0 then
+ Prio.avail = Prio.avail + avail
+ if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
+ self:Despool(Prio)
+ -- Note: We might not get here if the user-supplied callback function errors out! Take care!
+ end
+ end
+ end
+
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Spooling logic
+
+
+function ChatThrottleLib:Enqueue(prioname, pipename, msg)
+ local Prio = self.Prio[prioname]
+ local pipe = Prio.ByName[pipename]
+ if not pipe then
+ self.Frame:Show()
+ pipe = NewPipe()
+ pipe.name = pipename
+ Prio.ByName[pipename] = pipe
+ Prio.Ring:Add(pipe)
+ end
+
+ pipe[#pipe + 1] = msg
+
+ self.bQueueing = true
+end
+
+
+
+function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = text:len()
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendChatMessage(text, chattype, language, destination)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg)
+ end
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendChatMessage
+ msg[1] = text
+ msg[2] = chattype or "SAY"
+ msg[3] = language
+ msg[4] = destination
+ msg.n = 4
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
+end
+
+
+function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
+ if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
+ error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = prefix:len() + 1 + text:len();
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
+ end
+
+ nSize = nSize + self.MSG_OVERHEAD;
+
+ -- Check if there's room in the global available bandwidth gauge to send directly
+ if not self.bQueueing and nSize < self:UpdateAvail() then
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendAddonMessage(prefix, text, chattype, target)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg)
+ end
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendAddonMessage
+ msg[1] = prefix
+ msg[2] = text
+ msg[3] = chattype
+ msg[4] = target
+ msg.n = (target~=nil) and 4 or 3;
+ msg.nSize = nSize
+ msg.callbackFn = callbackFn
+ msg.callbackArg = callbackArg
+
+ self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
+end
+
+
+
+
+-----------------------------------------------------------------------
+-- Get the ball rolling!
+
+ChatThrottleLib:Init()
+
+--[[ WoWBench debugging snippet
+if(WOWB_VER) then
+ local function SayTimer()
+ print("SAY: "..GetTime().." "..arg1)
+ end
+ ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
+ ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
+end
+]]
+
+
diff --git a/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua
new file mode 100644
index 0000000..b072a2b
--- /dev/null
+++ b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua
@@ -0,0 +1,281 @@
+--- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
+-- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially
+-- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
+-- references to the same table will be send individually.
+--
+-- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by
+-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
+-- and can be accessed directly, without having to explicitly call AceSerializer itself.\\
+-- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
+-- make into AceSerializer.
+-- @class file
+-- @name AceSerializer-3.0
+-- @release $Id: AceSerializer-3.0.lua 910 2010-02-11 21:54:24Z mikk $
+local MAJOR,MINOR = "AceSerializer-3.0", 3
+local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceSerializer then return end
+
+-- Lua APIs
+local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format
+local assert, error, pcall = assert, error, pcall
+local type, tostring, tonumber = type, tostring, tonumber
+local pairs, select, frexp = pairs, select, math.frexp
+local tconcat = table.concat
+
+-- quick copies of string representations of wonky numbers
+local serNaN = tostring(0/0)
+local serInf = tostring(1/0)
+local serNegInf = tostring(-1/0)
+
+
+-- Serialization functions
+
+local function SerializeStringHelper(ch) -- Used by SerializeValue for strings
+ -- We use \126 ("~") as an escape character for all nonprints plus a few more
+ local n = strbyte(ch)
+ if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
+ return "\126\122"
+ elseif n<=32 then -- nonprint + space
+ return "\126"..strchar(n+64)
+ elseif n==94 then -- value separator
+ return "\126\125"
+ elseif n==126 then -- our own escape character
+ return "\126\124"
+ elseif n==127 then -- nonprint (DEL)
+ return "\126\123"
+ else
+ assert(false) -- can't be reached if caller uses a sane regex
+ end
+end
+
+local function SerializeValue(v, res, nres)
+ -- We use "^" as a value separator, followed by one byte for type indicator
+ local t=type(v)
+
+ if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc)
+ res[nres+1] = "^S"
+ res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
+ nres=nres+2
+
+ elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
+ local str = tostring(v)
+ if tonumber(str)==v or str==serNaN or str==serInf or str==serNegInf then
+ -- translates just fine, transmit as-is
+ res[nres+1] = "^N"
+ res[nres+2] = str
+ nres=nres+2
+ else
+ local m,e = frexp(v)
+ res[nres+1] = "^F"
+ res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999)
+ res[nres+3] = "^f"
+ res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation
+ nres=nres+4
+ end
+
+ elseif t=="table" then -- ^T...^t = table (list of key,value pairs)
+ nres=nres+1
+ res[nres] = "^T"
+ for k,v in pairs(v) do
+ nres = SerializeValue(k, res, nres)
+ nres = SerializeValue(v, res, nres)
+ end
+ nres=nres+1
+ res[nres] = "^t"
+
+ elseif t=="boolean" then -- ^B = true, ^b = false
+ nres=nres+1
+ if v then
+ res[nres] = "^B" -- true
+ else
+ res[nres] = "^b" -- false
+ end
+
+ elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P)
+ nres=nres+1
+ res[nres] = "^Z"
+
+ else
+ error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive
+ end
+
+ return nres
+end
+
+
+
+local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1
+
+--- Serialize the data passed into the function.
+-- Takes a list of values (strings, numbers, booleans, nils, tables)
+-- and returns it in serialized form (a string).\\
+-- May throw errors on invalid data types.
+-- @param ... List of values to serialize
+-- @return The data in its serialized form (string)
+function AceSerializer:Serialize(...)
+ local nres = 1
+
+ for i=1,select("#", ...) do
+ local v = select(i, ...)
+ nres = SerializeValue(v, serializeTbl, nres)
+ end
+
+ serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data
+
+ return tconcat(serializeTbl, "", 1, nres+1)
+end
+
+-- Deserialization functions
+local function DeserializeStringHelper(escape)
+ if escape<"~\122" then
+ return strchar(strbyte(escape,2,2)-64)
+ elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
+ return "\030"
+ elseif escape=="~\123" then
+ return "\127"
+ elseif escape=="~\124" then
+ return "\126"
+ elseif escape=="~\125" then
+ return "\94"
+ end
+ error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up
+end
+
+local function DeserializeNumberHelper(number)
+ if number == serNaN then
+ return 0/0
+ elseif number == serNegInf then
+ return -1/0
+ elseif number == serInf then
+ return 1/0
+ else
+ return tonumber(number)
+ end
+end
+
+-- DeserializeValue: worker function for :Deserialize()
+-- It works in two modes:
+-- Main (top-level) mode: Deserialize a list of values and return them all
+-- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
+--
+-- The function _always_ works recursively due to having to build a list of values to return
+--
+-- Callers are expected to pcall(DeserializeValue) to trap errors
+
+local function DeserializeValue(iter,single,ctl,data)
+
+ if not single then
+ ctl,data = iter()
+ end
+
+ if not ctl then
+ error("Supplied data misses AceSerializer terminator ('^^')")
+ end
+
+ if ctl=="^^" then
+ -- ignore extraneous data
+ return
+ end
+
+ local res
+
+ if ctl=="^S" then
+ res = gsub(data, "~.", DeserializeStringHelper)
+ elseif ctl=="^N" then
+ res = DeserializeNumberHelper(data)
+ if not res then
+ error("Invalid serialized number: '"..tostring(data).."'")
+ end
+ elseif ctl=="^F" then -- ^F^f
+ local ctl2,e = iter()
+ if ctl2~="^f" then
+ error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
+ end
+ local m=tonumber(data)
+ e=tonumber(e)
+ if not (m and e) then
+ error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
+ end
+ res = m*(2^e)
+ elseif ctl=="^B" then -- yeah yeah ignore data portion
+ res = true
+ elseif ctl=="^b" then -- yeah yeah ignore data portion
+ res = false
+ elseif ctl=="^Z" then -- yeah yeah ignore data portion
+ res = nil
+ elseif ctl=="^T" then
+ -- ignore ^T's data, future extensibility?
+ res = {}
+ local k,v
+ while true do
+ ctl,data = iter()
+ if ctl=="^t" then break end -- ignore ^t's data
+ k = DeserializeValue(iter,true,ctl,data)
+ if k==nil then
+ error("Invalid AceSerializer table format (no table end marker)")
+ end
+ ctl,data = iter()
+ v = DeserializeValue(iter,true,ctl,data)
+ if v==nil then
+ error("Invalid AceSerializer table format (no table end marker)")
+ end
+ res[k]=v
+ end
+ else
+ error("Invalid AceSerializer control code '"..ctl.."'")
+ end
+
+ if not single then
+ return res,DeserializeValue(iter)
+ else
+ return res
+ end
+end
+
+--- Deserializes the data into its original values.
+-- Accepts serialized data, ignoring all control characters and whitespace.
+-- @param str The serialized data (from :Serialize)
+-- @return true followed by a list of values, OR false followed by an error message
+function AceSerializer:Deserialize(str)
+ str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff
+
+ local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
+ local ctl,data = iter()
+ if not ctl or ctl~="^1" then
+ -- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
+ return false, "Supplied data is not AceSerializer data (rev 1)"
+ end
+
+ return pcall(DeserializeValue, iter)
+end
+
+
+----------------------------------------
+-- Base library stuff
+----------------------------------------
+
+AceSerializer.internals = { -- for test scripts
+ SerializeValue = SerializeValue,
+ SerializeStringHelper = SerializeStringHelper,
+}
+
+local mixins = {
+ "Serialize",
+ "Deserialize",
+}
+
+AceSerializer.embeds = AceSerializer.embeds or {}
+
+function AceSerializer:Embed(target)
+ for k, v in pairs(mixins) do
+ target[v] = self[v]
+ end
+ self.embeds[target] = true
+ return target
+end
+
+-- Update embeds
+for target, v in pairs(AceSerializer.embeds) do
+ AceSerializer:Embed(target)
+end
\ No newline at end of file
diff --git a/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml
new file mode 100644
index 0000000..94924af
--- /dev/null
+++ b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua
new file mode 100644
index 0000000..fdb2cff
--- /dev/null
+++ b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua
@@ -0,0 +1,473 @@
+--- **AceTimer-3.0** provides a central facility for registering timers.
+-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
+-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered, rescheduled
+-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
+-- AceTimer is currently limited to firing timers at a frequency of 0.1s. This constant may change
+-- in the future, but for now it seemed like a good compromise in efficiency and accuracy.
+--
+-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
+-- need to cancel or reschedule the timer you just registered.
+--
+-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer:Embed(MyAddon) or by
+-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
+-- and can be accessed directly, without having to explicitly call AceTimer itself.\\
+-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
+-- make into AceTimer.
+-- @class file
+-- @name AceTimer-3.0
+-- @release $Id: AceTimer-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
+
+--[[
+ Basic assumptions:
+ * In a typical system, we do more re-scheduling per second than there are timer pulses per second
+ * Regardless of timer implementation, we cannot guarantee timely delivery due to FPS restriction (may be as low as 10)
+
+ This implementation:
+ CON: The smallest timer interval is constrained by HZ (currently 1/10s).
+ PRO: It will still correctly fire any timer slower than HZ over a length of time, e.g. 0.11s interval -> 90 times over 10 seconds
+ PRO: In lag bursts, the system simly skips missed timer intervals to decrease load
+ CON: Algorithms depending on a timer firing "N times per minute" will fail
+ PRO: (Re-)scheduling is O(1) with a VERY small constant. It's a simple linked list insertion in a hash bucket.
+ CAUTION: The BUCKETS constant constrains how many timers can be efficiently handled. With too many hash collisions, performance will decrease.
+
+ Major assumptions upheld:
+ - ALLOWS scheduling multiple timers with the same funcref/method
+ - ALLOWS scheduling more timers during OnUpdate processing
+ - ALLOWS unscheduling ANY timer (including the current running one) at any time, including during OnUpdate processing
+]]
+
+local MAJOR, MINOR = "AceTimer-3.0", 5
+local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceTimer then return end -- No upgrade needed
+
+AceTimer.hash = AceTimer.hash or {} -- Array of [0..BUCKET-1] = linked list of timers (using .next member)
+ -- Linked list gets around ACE-88 and ACE-90.
+AceTimer.selfs = AceTimer.selfs or {} -- Array of [self]={[handle]=timerobj, [handle2]=timerobj2, ...}
+AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame")
+
+-- Lua APIs
+local assert, error, loadstring = assert, error, loadstring
+local setmetatable, rawset, rawget = setmetatable, rawset, rawget
+local select, pairs, type, next, tostring = select, pairs, type, next, tostring
+local floor, max, min = math.floor, math.max, math.min
+local tconcat = table.concat
+
+-- WoW APIs
+local GetTime = GetTime
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: DEFAULT_CHAT_FRAME, geterrorhandler
+
+-- Simple ONE-SHOT timer cache. Much more efficient than a full compost for our purposes.
+local timerCache = nil
+
+--[[
+ Timers will not be fired more often than HZ-1 times per second.
+ Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999)
+ If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade.
+ If this number is ever changed, all entries need to be rehashed on lib upgrade.
+ ]]
+local HZ = 11
+
+--[[
+ Prime for good distribution
+ If this number is ever changed, all entries need to be rehashed on lib upgrade.
+]]
+local BUCKETS = 131
+
+local hash = AceTimer.hash
+for i=1,BUCKETS do
+ hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes
+end
+
+--[[
+ xpcall safecall implementation
+]]
+local xpcall = xpcall
+
+local function errorhandler(err)
+ return geterrorhandler()(err)
+end
+
+local function CreateDispatcher(argCount)
+ local code = [[
+ local xpcall, eh = ... -- our arguments are received as unnamed values in "..." since we don't have a proper function declaration
+ local method, ARGS
+ local function call() return method(ARGS) end
+
+ local function dispatch(func, ...)
+ method = func
+ if not method then return end
+ ARGS = ...
+ return xpcall(call, eh)
+ end
+
+ return dispatch
+ ]]
+
+ local ARGS = {}
+ for i = 1, argCount do ARGS[i] = "arg"..i end
+ code = code:gsub("ARGS", tconcat(ARGS, ", "))
+ return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
+end
+
+local Dispatchers = setmetatable({}, {
+ __index=function(self, argCount)
+ local dispatcher = CreateDispatcher(argCount)
+ rawset(self, argCount, dispatcher)
+ return dispatcher
+ end
+})
+Dispatchers[0] = function(func)
+ return xpcall(func, errorhandler)
+end
+
+local function safecall(func, ...)
+ return Dispatchers[select('#', ...)](func, ...)
+end
+
+local lastint = floor(GetTime() * HZ)
+
+-- --------------------------------------------------------------------
+-- OnUpdate handler
+--
+-- traverse buckets, always chasing "now", and fire timers that have expired
+
+local function OnUpdate()
+ local now = GetTime()
+ local nowint = floor(now * HZ)
+
+ -- Have we passed into a new hash bucket?
+ if nowint == lastint then return end
+
+ local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2
+
+ -- Pass through each bucket at most once
+ -- Happens on e.g. instance loads, but COULD happen on high local load situations also
+ for curint = (max(lastint, nowint - BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteration
+ local curbucket = (curint % BUCKETS)+1
+ -- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks.
+ local nexttimer = hash[curbucket]
+ hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash
+
+ while nexttimer do
+ local timer = nexttimer
+ nexttimer = timer.next
+ local when = timer.when
+
+ if when < soon then
+ -- Call the timer func, either as a method on given object, or a straight function ref
+ local callback = timer.callback
+ if type(callback) == "string" then
+ safecall(timer.object[callback], timer.object, timer.arg)
+ elseif callback then
+ safecall(callback, timer.arg)
+ else
+ -- probably nilled out by CancelTimer
+ timer.delay = nil -- don't reschedule it
+ end
+
+ local delay = timer.delay -- NOW make a local copy, can't do it earlier in case the timer cancelled itself in the callback
+
+ if not delay then
+ -- single-shot timer (or cancelled)
+ AceTimer.selfs[timer.object][tostring(timer)] = nil
+ timerCache = timer
+ else
+ -- repeating timer
+ local newtime = when + delay
+ if newtime < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.)
+ newtime = now + delay
+ end
+ timer.when = newtime
+
+ -- add next timer execution to the correct bucket
+ local bucket = (floor(newtime * HZ) % BUCKETS) + 1
+ timer.next = hash[bucket]
+ hash[bucket] = timer
+ end
+ else -- if when>=soon
+ -- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution)
+ timer.next = hash[curbucket]
+ hash[curbucket] = timer
+ end -- if whenhandle->timer registry
+ local handle = tostring(timer)
+
+ local selftimers = AceTimer.selfs[self]
+ if not selftimers then
+ selftimers = {}
+ AceTimer.selfs[self] = selftimers
+ end
+ selftimers[handle] = timer
+ selftimers.__ops = (selftimers.__ops or 0) + 1
+
+ return handle
+end
+
+--- Schedule a new one-shot timer.
+-- The timer will fire once in `delay` seconds, unless canceled before.
+-- @param callback Callback function for the timer pulse (funcref or method name).
+-- @param delay Delay for the timer, in seconds.
+-- @param arg An optional argument to be passed to the callback function.
+-- @usage
+-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0")
+--
+-- function MyAddon:OnEnable()
+-- self:ScheduleTimer("TimerFeedback", 5)
+-- end
+--
+-- function MyAddon:TimerFeedback()
+-- print("5 seconds passed")
+-- end
+function AceTimer:ScheduleTimer(callback, delay, arg)
+ return Reg(self, callback, delay, arg)
+end
+
+--- Schedule a repeating timer.
+-- The timer will fire every `delay` seconds, until canceled.
+-- @param callback Callback function for the timer pulse (funcref or method name).
+-- @param delay Delay for the timer, in seconds.
+-- @param arg An optional argument to be passed to the callback function.
+-- @usage
+-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0")
+--
+-- function MyAddon:OnEnable()
+-- self.timerCount = 0
+-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
+-- end
+--
+-- function MyAddon:TimerFeedback()
+-- self.timerCount = self.timerCount + 1
+-- print(("%d seconds passed"):format(5 * self.timerCount))
+-- -- run 30 seconds in total
+-- if self.timerCount == 6 then
+-- self:CancelTimer(self.testTimer)
+-- end
+-- end
+function AceTimer:ScheduleRepeatingTimer(callback, delay, arg)
+ return Reg(self, callback, delay, arg, true)
+end
+
+--- Cancels a timer with the given handle, registered by the same addon object as used for `:ScheduleTimer`
+-- Both one-shot and repeating timers can be canceled with this function, as long as the `handle` is valid
+-- and the timer has not fired yet or was canceled before.
+-- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
+-- @param silent If true, no error is raised if the timer handle is invalid (expired or already canceled)
+-- @return True if the timer was successfully cancelled.
+function AceTimer:CancelTimer(handle, silent)
+ if not handle then return end -- nil handle -> bail out without erroring
+ if type(handle) ~= "string" then
+ error(MAJOR..": CancelTimer(handle): 'handle' - expected a string", 2) -- for now, anyway
+ end
+ local selftimers = AceTimer.selfs[self]
+ local timer = selftimers and selftimers[handle]
+ if silent then
+ if timer then
+ timer.callback = nil -- don't run it again
+ timer.delay = nil -- if this is the currently-executing one: don't even reschedule
+ -- The timer object is removed in the OnUpdate loop
+ end
+ return not not timer -- might return "true" even if we double-cancel. we'll live.
+ else
+ if not timer then
+ geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - no such timer registered")
+ return false
+ end
+ if not timer.callback then
+ geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - timer already cancelled or expired")
+ return false
+ end
+ timer.callback = nil -- don't run it again
+ timer.delay = nil -- if this is the currently-executing one: don't even reschedule
+ return true
+ end
+end
+
+--- Cancels all timers registered to the current addon object ('self')
+function AceTimer:CancelAllTimers()
+ if not(type(self) == "string" or type(self) == "table") then
+ error(MAJOR..": CancelAllTimers(): 'self' - must be a string or a table",2)
+ end
+ if self == AceTimer then
+ error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2)
+ end
+
+ local selftimers = AceTimer.selfs[self]
+ if selftimers then
+ for handle,v in pairs(selftimers) do
+ if type(v) == "table" then -- avoid __ops, etc
+ AceTimer.CancelTimer(self, handle, true)
+ end
+ end
+ end
+end
+
+--- Returns the time left for a timer with the given handle, registered by the current addon object ('self').
+-- This function will raise a warning when the handle is invalid, but not stop execution.
+-- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
+-- @return The time left on the timer, or false if the handle is invalid.
+function AceTimer:TimeLeft(handle)
+ if not handle then return end
+ if type(handle) ~= "string" then
+ error(MAJOR..": TimeLeft(handle): 'handle' - expected a string", 2) -- for now, anyway
+ end
+ local selftimers = AceTimer.selfs[self]
+ local timer = selftimers and selftimers[handle]
+ if not timer then
+ geterrorhandler()(MAJOR..": TimeLeft(handle): '"..tostring(handle).."' - no such timer registered")
+ return false
+ end
+ return timer.when - GetTime()
+end
+
+
+-- ---------------------------------------------------------------------
+-- PLAYER_REGEN_ENABLED: Run through our .selfs[] array step by step
+-- and clean it out - otherwise the table indices can grow indefinitely
+-- if an addon starts and stops a lot of timers. AceBucket does this!
+--
+-- See ACE-94 and tests/AceTimer-3.0-ACE-94.lua
+
+local lastCleaned = nil
+
+local function OnEvent(this, event)
+ if event~="PLAYER_REGEN_ENABLED" then
+ return
+ end
+
+ -- Get the next 'self' to process
+ local selfs = AceTimer.selfs
+ local self = next(selfs, lastCleaned)
+ if not self then
+ self = next(selfs)
+ end
+ lastCleaned = self
+ if not self then -- should only happen if .selfs[] is empty
+ return
+ end
+
+ -- Time to clean it out?
+ local list = selfs[self]
+ if (list.__ops or 0) < 250 then -- 250 slosh indices = ~10KB wasted (max!). For one 'self'.
+ return
+ end
+
+ -- Create a new table and copy all members over
+ local newlist = {}
+ local n=0
+ for k,v in pairs(list) do
+ newlist[k] = v
+ n=n+1
+ end
+ newlist.__ops = 0 -- Reset operation count
+
+ -- And since we now have a count of the number of live timers, check that it's reasonable. Emit a warning if not.
+ if n>BUCKETS then
+ DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(self).."' has "..n.." live timers. Surely that's not intended?")
+ end
+
+ selfs[self] = newlist
+end
+
+-- ---------------------------------------------------------------------
+-- Embed handling
+
+AceTimer.embeds = AceTimer.embeds or {}
+
+local mixins = {
+ "ScheduleTimer", "ScheduleRepeatingTimer",
+ "CancelTimer", "CancelAllTimers",
+ "TimeLeft"
+}
+
+function AceTimer:Embed(target)
+ AceTimer.embeds[target] = true
+ for _,v in pairs(mixins) do
+ target[v] = AceTimer[v]
+ end
+ return target
+end
+
+-- AceTimer:OnEmbedDisable( target )
+-- target (object) - target object that AceTimer is embedded in.
+--
+-- cancel all timers registered for the object
+function AceTimer:OnEmbedDisable( target )
+ target:CancelAllTimers()
+end
+
+
+for addon in pairs(AceTimer.embeds) do
+ AceTimer:Embed(addon)
+end
+
+-- ---------------------------------------------------------------------
+-- Debug tools (expose copies of internals to test suites)
+AceTimer.debug = AceTimer.debug or {}
+AceTimer.debug.HZ = HZ
+AceTimer.debug.BUCKETS = BUCKETS
+
+-- ---------------------------------------------------------------------
+-- Finishing touchups
+
+AceTimer.frame:SetScript("OnUpdate", OnUpdate)
+AceTimer.frame:SetScript("OnEvent", OnEvent)
+AceTimer.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
+
+-- In theory, we should hide&show the frame based on there being timers or not.
+-- However, this job is fairly expensive, and the chance that there will
+-- actually be zero timers running is diminuitive to say the lest.
diff --git a/Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml
new file mode 100644
index 0000000..38e9021
--- /dev/null
+++ b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua
new file mode 100644
index 0000000..247a887
--- /dev/null
+++ b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua
@@ -0,0 +1,2352 @@
+------------------------------------------------------------------------
+-- SpecializedAbsorbs
+------------------------------------------------------------------------
+
+local MAJOR, MINOR = "SpecializedAbsorbs-1.0", 4
+local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not lib then return end
+local Core
+
+local error = error
+local pairs, select = pairs, select
+local min, max, floor = math.min, math.max, math.floor
+local setmetatable, getmetatable = setmetatable, getmetatable
+local tinsert, tremove, tsort = table.insert, table.remove, table.sort
+local UnitBuff, UnitHealthMax, UnitAttackPower = UnitBuff, UnitHealthMax, UnitAttackPower
+local UnitExists, UnitGUID, UnitClass, UnitLevel = UnitExists, UnitGUID, UnitClass, UnitLevel
+local UnitFactionGroup, UnitInBattleground, GetTalentInfo = UnitFactionGroup, UnitInBattleground, GetTalentInfo
+local GetNumRaidMembers, GetNumPartyMembers = GetNumRaidMembers, GetNumPartyMembers
+
+lib.CheckFlags = lib.CheckFlags or true
+lib.NoErrors = lib.NoErrors or true
+
+---------------------
+-- Install/Upgrade --
+---------------------
+-- We have to upgrade from a previous version
+if oldminor then
+ Core = lib.Core
+ if lib.Enabled then
+ Core.Disable()
+ end
+else
+ lib.Core = {callbacks = LibStub:GetLibrary("CallbackHandler-1.0"):New(lib), Events = {}}
+ Core = lib.Core
+
+ local frame = CreateFrame("Frame", "SpecializedAbsorbs_Events")
+ local events = Core.Events
+ frame:SetScript("OnEvent", function(self, event, ...) events[event](...) end)
+ Core.Frame = frame
+ LibStub("AceComm-3.0"):Embed(Core)
+ LibStub("AceTimer-3.0"):Embed(Core)
+ LibStub("AceSerializer-3.0"):Embed(Core)
+end
+
+---------------------
+-- Local Variables --
+---------------------
+
+local callbacks = Core.callbacks
+local Events = Core.Events
+
+local playerid, playerclass
+
+-- Specifies the channel any AddOn message should be
+-- sent to, nil if silent
+-- Can also be used to get the last known party state
+local curChatChannel = nil
+
+-- Hold the group type and number of members
+local groupCount = 0
+
+-- Always hold the timestamp from the last COMBAT_LOG_EVENT_UNFILTERED fired
+local lastCombatLogEvent = 0.0
+
+-- Table of all active absorb effects indexed by GUID and then spellid
+-- at spellid == -1 there is a numeric entry with the total remaining value,
+-- at spellid == -2 there is a numeric entry with the total quality (that is, the minimal quality)
+-- The priority is also in this table for performance reasons during sort
+-- [GUID] = { [spellid] = {spellid, priority, remainingValue, maxValue, quality, durationTimerHandle, extra} }
+-- Shortcut to Core.activeEffects.bySpell
+local activeEffectsBySpell
+
+-- Table of all active absorb effects indexed by GUID and then a list in the order in which
+-- they will be used
+-- Shortcut to Core.activeEffects.byPriority
+local activeEffectsByPriority
+
+-- Table of all active area absorb effects indexed by triggerGUID
+-- same format as an entry in activeEffectsBySpell with two extra fields at the end
+-- {..., triggerGUID, refcount}
+-- Note that in this approach, a trigger can have only one absorb effect up at the same time.
+-- This should be a reasonable assumption for quite some time, considering only Anti-Magic Zone
+-- and (soon) Power Word: Barrier use this feature anyway
+-- Shortcut to Core.activeEffects.Area
+local activeAreaEffects
+
+-- Table of current unit charges
+-- A charge is a variant value a custom trigger can put on any unit with a limited lifetime
+-- It is organized as a (very simple) queue (FIFO) for use with Divine Aegis e.g. to save the
+-- critical heal value and then apply it on the aura gain or with Val'anyr.
+-- [GUID] = { [spellid] = { charge1, charge2, ... } }
+-- Shortcut to Core.activeCharges
+local activeCharges
+
+-- Table of known spells that cause an absorb effects
+-- priority: active effects above 2 are neither used in total value nor displayed (e.g. Anti-Magic Shell)
+-- [spellid] = {priority, duration, createFunc, hitFunc}
+-- Shortcut to Core.EffectInfo
+local Effects
+
+-- Table of spells that cause an area effect
+-- [spellid (trigger)] = spellid (absorb effect)
+-- Shortcut to Core.AreaTriggers
+local AreaTriggers
+
+-- Table of additional callbacks on combat log events for proc-based and other non-generic absorb effects.
+-- Shortcuts to entries of Core.CombatTriggers
+local CombatTriggersOnHeal
+local CombatTriggersOnHealCrit
+local CombatTriggersOnAuraApplied
+local CombatTriggersOnAuraRemoved
+
+-- Table of all unit stats relevant to absorb effects like attack power and spell power
+-- (mastery rating later on)
+-- [GUID] = { class, AttackPower, SpellPower, quality }
+-- Shortcut to Core.UnitStats
+local UnitStatsTable
+
+-- Table of all scaling factors to absorb effects like talents, items, set boni, buffs
+-- If there is no mechanism in Cataclysm to obtain the correct absorb amount by any effect
+-- this entries are meant to be distributed among a group, raid, etc
+-- [GUID] = { [scaling_Name] = scaling_Value }
+-- A numerical scaling_name above 10 should ONLY be used if it is the spellid of the affected effect,
+-- since it is sometimes used as a very quick way to check for a unit's class
+-- Scaling factors that are only relevant to the local user like priest talents and do not
+-- need to be distributed but are needed for calculation of the public one's are private ones,
+-- found at index -1
+-- Shortcuts to Core.Scaling and its entries
+local Scaling
+local privateScaling
+local playerScaling
+
+-- Table of all unitGUIDs that have a Heal Absorb spell effect, with shield amount as value
+-- Health threshold until it gets fully absorbed
+local GUIDtoAbsorbHealSpells
+
+-- Class-specific callbacks
+local OnEnableClass = {}
+local OnScalingDecode = setmetatable({}, {__index = function(table, class) return table.DEFAULT end})
+
+-- Shortcut to the most important core functions
+local ApplySingularEffect
+local ApplyAreaEffect
+local CreateAreaTrigger
+local HitUnit
+local RemoveActiveEffect
+
+-- Constants
+local LOW_VALUE_TOLERANCE = 50
+local ZONE_MODIFIER = 1
+
+-- addon comm prefixes
+local COMM_UNITSTATS = "SpecializedAbsorbs_UnitStats"
+local COMM_SCALING = "SpecializedAbsorbs_Scaling"
+
+-- for players using AbsorbsMonitor-1.0
+local COMM_UNITSTATS_ALT = "Absorbs_UnitStats"
+local COMM_SCALING_ALT = "Absorbs_Scaling"
+
+----------------------
+-- Helper functions --
+----------------------
+
+local CommStatsCooldown = false
+local function ClearCommStatsCooldown()
+ CommStatsCooldown = false
+end
+
+local CommScalingCooldown = false
+local function ClearCommScalingCooldown()
+ CommScalingCooldown = false
+end
+
+local function DeepTableCopy(src)
+ local res = {}
+
+ for k, v in pairs(src) do
+ if type(k) == "table" then
+ k = DeepTableCopy(k)
+ end
+ if type(v) == "table" then
+ v = DeepTableCopy(v)
+ end
+ res[k] = v
+ end
+ setmetatable(res, getmetatable(src))
+ return res
+end
+
+local function NoOp() end
+
+local SortEffects
+do
+ local mage_fire_ward = {543, 8457, 8458, 10223, 10225, 27128, 43010}
+ local mage_frost_ward = {6143, 8461, 8462, 10177, 28609, 32796, 43012}
+ local mage_ice_barrier = {11426, 13031, 13032, 13033, 27134, 33405, 43038, 43039}
+ local warlock_shadow_ward = {6229, 11739, 11740, 28610, 47890, 47891}
+ local warlock_sacrifice = {7812, 19438, 19440, 19441, 19442, 19443, 27273, 47985, 47986}
+
+ function SortEffects(a, b)
+ -- use timestamp in case of the same id
+ if a[1] == b[1] then
+ if a[8] == nil then
+ return true
+ elseif b[8] == nil then
+ return false
+ else
+ return (a[8] < b[8])
+ end
+ end
+
+ -- Twin Val'kyr
+ if a[1] == 65686 then
+ return true
+ end
+ if b[1] == 65686 then
+ return false
+ end
+ if a[1] == 65684 then
+ return true
+ end
+ if b[1] == 65684 then
+ return false
+ end
+
+ -- Frost Ward
+ if tContains(mage_frost_ward, a[1]) then
+ return true
+ end
+ if tContains(mage_frost_ward, b[1]) then
+ return false
+ end
+
+ -- Fire Ward
+ if tContains(mage_fire_ward, a[1]) then
+ return true
+ end
+ if tContains(mage_fire_ward, b[1]) then
+ return false
+ end
+
+ -- Shadow Ward
+ if tContains(warlock_shadow_ward, a[1]) then
+ return true
+ end
+ if tContains(warlock_shadow_ward, b[1]) then
+ return false
+ end
+
+ -- Sacred Shield
+ if a[1] == 58597 then
+ return true
+ end
+ if b[1] == 58597 then
+ return false
+ end
+
+ -- Fel Blossom
+ if a[1] == 58597 then
+ return true
+ end
+ if b[1] == 58597 then
+ return false
+ end
+
+ -- Divine Aegis
+ if a[1] == 47753 then
+ return true
+ end
+ if b[1] == 47753 then
+ return false
+ end
+
+ -- Ice Barrier
+ if tContains(mage_ice_barrier, a[1]) then
+ return true
+ end
+ if tContains(mage_ice_barrier, b[1]) then
+ return false
+ end
+
+ -- Sacrifice
+ if tContains(warlock_sacrifice, a[1]) then
+ return true
+ end
+ if tContains(warlock_sacrifice, b[1]) then
+ return false
+ end
+
+ -- same priority
+ -- if a[2] == b[2] then
+ -- return (a[3] < b[3])
+ -- elseif a[2] and a[2] then
+ -- return (a[3] > b[3])
+ -- end
+
+ -- latest.
+ return (a[7] < b[7])
+ end
+end
+
+-- Tries to get a working unitId
+local function GetUnitId(guid, name)
+ if name and UnitGUID(name) then
+ return name
+ end
+ if UnitGUID("target") == guid then
+ return "target"
+ end
+ if UnitGUID("focus") == guid then
+ return "focus"
+ end
+
+ -- group
+ local prefix
+ if curChatChannel == "BATTLEGROUND" or curChatChannel == "RAID" then
+ prefix = "raid"
+ elseif curChatChannel == "PARTY" then
+ prefix = "party"
+ end
+
+ if prefix then
+ for i = 1, groupCount do
+ local unit = (i == 0) and "player" or prefix .. i
+ if UnitExists(unit) then
+ if UnitGUID(unit) == guid then
+ return unit
+ end
+ if UnitExists(unit .. "pet") and UnitGUID(unit .. "pet") == guid then
+ return unit .. "pet"
+ end
+ end
+ end
+ end
+ -- solo
+ if UnitGUID("player") == guid then
+ return "player"
+ end
+ if UnitExists("playerpet") and UnitGUID("playerpet") == guid then
+ return "playerpet"
+ end
+end
+
+-- Tries to get a working unitId and calls the given UnitXXX() function
+local function UnitDispatch(func, guid, name)
+ local result = func(name)
+ if not result then
+ local unit = GetUnitId(guid, name)
+ if unit then
+ result = func(unit)
+ end
+ end
+ return result
+end
+
+--------------------
+-- Core functions --
+--------------------
+
+function Core.Error(...)
+ if lib.NoErrors then return end
+ error(...)
+end
+
+function Core.Enable()
+ playerclass = select(2, UnitClass("player"))
+ playerid = UnitGUID("player")
+
+ Core.activeEffects = {bySpell = {}, byPriority = {}, Area = {}}
+
+ activeEffectsBySpell = Core.activeEffects.bySpell
+ activeEffectsByPriority = Core.activeEffects.byPriority
+ activeAreaEffects = Core.activeEffects.Area
+
+ Core.activeCharges = {}
+ activeCharges = Core.activeCharges
+
+ Effects = Core.Effects
+ AreaTriggers = Core.AreaTriggers
+
+ CombatTriggersOnHeal = Core.CombatTriggers.OnHeal
+ CombatTriggersOnHealCrit = Core.CombatTriggers.OnHealCrit
+ CombatTriggersOnAuraApplied = Core.CombatTriggers.OnAuraApplied
+ CombatTriggersOnAuraRemoved = Core.CombatTriggers.OnAuraRemoved
+
+ Core.UnitStatsTable = {[playerid] = {playerclass, 0, 0, 1.0}}
+ UnitStatsTable = Core.UnitStatsTable
+
+ Core.Scaling = {[-1] = {}, [playerid] = {}}
+ Scaling = Core.Scaling
+
+ playerScaling = Scaling[playerid]
+ privateScaling = Scaling[-1]
+
+ Core.GUIDtoAbsorbHealSpells = {}
+ GUIDtoAbsorbHealSpells = Core.GUIDtoAbsorbHealSpells
+
+ Core.RegisterEvent("ZONE_CHANGED_NEW_AREA")
+ Events.ZONE_CHANGED_NEW_AREA()
+
+ if playerclass == "DEATHKNIGHT" then
+ Core.RegisterEvent("UNIT_ATTACK_POWER")
+ elseif playerclass == "DRUID" then
+ Core.RegisterEvent("UNIT_ATTACK_POWER")
+ elseif playerclass == "MAGE" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ elseif playerclass == "PALADIN" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ elseif playerclass == "PRIEST" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ elseif playerclass == "WARLOCK" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ end
+
+ Events.STATS_CHANGED()
+
+ -- This has to happen before class init, else we get no initial scaling broadcast
+ if not Core.Silent then
+ Core.SetVerbose()
+ end
+
+ if OnEnableClass[playerclass] then
+ OnEnableClass[playerclass]()
+ end
+
+ if Events.PLAYER_LEVEL_UP then
+ Core.RegisterEvent("PLAYER_LEVEL_UP")
+ end
+
+ if Events.PLAYER_TALENT_UPDATE then
+ Core.RegisterEvent("PLAYER_TALENT_UPDATE")
+ end
+
+ if Events.GLYPH_UPDATED then
+ Core.RegisterEvent("GLYPH_UPDATED")
+ end
+
+ if Events.PLAYER_EQUIPMENT_CHANGED then
+ Core.RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+ end
+
+ Core:ScheduleRepeatingTimer(Events.OnPeriodicBroadcast, 300)
+
+ Core:RegisterComm(COMM_UNITSTATS, Events.OnUnitStatsReceived)
+ Core:RegisterComm(COMM_UNITSTATS_ALT, Events.OnUnitStatsReceived)
+
+ Core:RegisterComm(COMM_SCALING, Events.OnScalingReceived)
+ Core:RegisterComm(COMM_SCALING_ALT, Events.OnScalingReceived)
+
+ if not lib.Passive then
+ Core:SetActive()
+ end
+
+ lib.Enabled = true
+end
+
+-- These function has to clear any memory this version of the library may
+-- have accumulated. It will be called in case this library version gets
+-- replaced by a new one
+function Core.Disable()
+ for guid, effects in pairs(activeEffectsBySpell) do
+ callbacks:Fire("UnitCleared", guid)
+ end
+
+ wipe(Core.activeEffects)
+ wipe(Core.activeCharges)
+
+ wipe(Core.Effects)
+ wipe(Core.CombatTriggers)
+ wipe(Core.AreaTriggers)
+
+ wipe(Core.UnitStatsTable)
+ wipe(Core.Scaling)
+
+ wipe(Core.GUIDtoAbsorbHealSpells)
+
+ wipe(Core.Events)
+ Core.Frame:UnregisterAllEvents()
+
+ Core:UnregisterAllComm()
+ Core:CancelAllTimers()
+
+ lib.Enabled = false
+
+ collectgarbage("collect")
+end
+
+function Core.RegisterEvent(name)
+ Core.Frame:RegisterEvent(name)
+end
+
+function Core.UnregisterEvent(name)
+ Core.Frame:UnregisterEvent(name)
+end
+
+local Debug = false
+function Core.Debug(str)
+ if not Debug then return end
+ Core.Print(str)
+end
+
+function Core.Print(str)
+ DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99AbsMon|r: " .. str)
+end
+
+function Core.ApplySingularEffect(timestamp, srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ local destEffects = activeEffectsBySpell[dstGUID]
+ local effectInfo = Effects[spellid]
+ local effectEntry
+
+ local value, quality = effectInfo[3](srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ if value == nil then return end
+
+ -- No entry yet for this unit
+ if not destEffects then
+ -- Not this specific effect yet
+ effectEntry = {spellid, effectInfo[1], value, value, quality, 0, timestamp}
+ destEffects = {[-1] = 0, [-2] = 1.0, [spellid] = effectEntry}
+ activeEffectsBySpell[dstGUID] = destEffects
+ activeEffectsByPriority[dstGUID] = {effectEntry}
+ callbacks:Fire("EffectApplied", srcGUID, srcName, dstGUID, dstName, spellid, spellschool, value, quality, effectInfo[2])
+ elseif not destEffects[spellid] then
+ -- Effect exists already
+ effectEntry = {spellid, effectInfo[1], value, value, quality, 0, timestamp}
+ destEffects[spellid] = effectEntry
+ tinsert(activeEffectsByPriority[dstGUID], effectEntry)
+ tsort(activeEffectsByPriority[dstGUID], SortEffects)
+ callbacks:Fire("EffectApplied", srcGUID, srcName, dstGUID, dstName, spellid, spellschool, value, quality, effectInfo[2])
+ else
+ effectEntry = destEffects[spellid]
+ local prevAmount = effectEntry[3]
+
+ effectEntry[3] = value
+ effectEntry[4] = value
+ effectEntry[5] = quality
+ effectEntry[7] = timestamp
+
+ tsort(activeEffectsByPriority[dstGUID], SortEffects)
+ callbacks:Fire("EffectUpdated", dstGUID, spellid, value, quality, effectInfo[2])
+
+ -- Adjust value in case this is a visible absorb to get the difference
+ value = value - prevAmount
+
+ -- Cancel the exting duration timeout timer
+ Core:CancelTimer(effectEntry[6], true)
+ end
+
+ if quality < destEffects[-2] then
+ destEffects[-2] = quality
+ end
+
+ if effectInfo[1] < 2 then
+ destEffects[-1] = destEffects[-1] + value
+ callbacks:Fire("UnitUpdated", dstGUID, destEffects[-1], destEffects[-2])
+ end
+
+ if effectInfo[2] then
+ -- We add a 5s grace period for latency and all kind of stuff
+ -- This duration timeout should only be needed if the unit moved out of combat log
+ -- reporting range anyway
+ effectEntry[6] = Core:ScheduleTimer(Events.OnSingularTimeout, effectInfo[2] + 5, {dstGUID, spellid})
+ else
+ effectEntry[6] = Core:ScheduleRepeatingTimer(Events.OnSingularActivityCheck, 8, {dstGUID, spellid})
+ end
+end
+
+function Core.ApplyAreaEffect(timestamp, triggerGUID, triggerName, dstGUID, dstName, spellid, spellschool)
+ if not activeAreaEffects[triggerGUID] then
+ return ApplySingularEffect(timestamp, triggerGUID, triggerName, dstGUID, dstName, 0, spellschool)
+ end
+
+ local effectEntry = activeAreaEffects[triggerGUID]
+ local destEffects = activeEffectsBySpell[dstGUID]
+
+ if not destEffects then
+ -- At the moment it is impossible for such an effect to be refreshed
+ -- Since it is created by a summoned unit radiating it, it either
+ -- gets removed/reapplied or removed/applied by a different unit.
+ -- Quality of 1.1 to enforce message
+ destEffects = {[-1] = 0, [-2] = 1.1}
+
+ activeEffectsBySpell[dstGUID] = destEffects
+ activeEffectsByPriority[dstGUID] = {}
+ elseif destEffects[spellid] then
+ Core.Error("Called ApplyAreaEffect on refreshed aura")
+ return
+ end
+
+ destEffects[spellid] = effectEntry
+ effectEntry[9] = effectEntry[9] + 1 -- increase refcount
+
+ -- While we keep the trigger itself as a normal afflicted unit to keep
+ -- track when the area effect breaks, we do not broadcast it nor have to
+ -- keep a sorted priority list
+ if triggerGUID ~= dstGUID then
+ tinsert(activeEffectsByPriority[dstGUID], effectEntry)
+ tsort(activeEffectsByPriority[dstGUID], SortEffects)
+
+ -- Note that we CANNOT use nil as an amount, since external addons can rely on this value being non-nil
+ -- for sorting. We're using -1 here that usually represents infinite values
+ callbacks:Fire("EffectApplied", triggerGUID, triggerName, dstGUID, dstName, spellid, spellschool, -1, effectEntry[5], nil)
+
+ -- Update quality if needed
+ if effectEntry[5] < destEffects[-2] then
+ destEffects[-2] = effectEntry[5]
+ callbacks:Fire("UnitUpdated", dstGUID, destEffects[-1], destEffects[-2])
+ end
+ end
+end
+
+function Core.CreateAreaTrigger(timestamp, srcGUID, srcName, triggerGUID, triggerName, spellid, spellschool)
+ if activeAreaEffects[triggerGUID] then
+ Core.Error("Trying to create new area trigger on existing one, triggerGUID: " .. triggerGUID .. ", existing spellid: " .. activeAreaEffects[triggerGUID][1])
+ return
+ end
+
+ local effectInfo = Effects[spellid]
+ local value, quality = effectInfo[3](srcGUID, srcName, triggerGUID, triggerName, spellid, nil)
+ if value == nil then return end
+
+ local effectEntry = {spellid, -1 * effectInfo[1], value, value, quality, 0, timestamp, triggerGUID, 0}
+ effectEntry[6] = Core:ScheduleTimer(Events.OnAreaTimeout, effectInfo[2] + 5, effectEntry)
+ activeAreaEffects[triggerGUID] = effectEntry
+ callbacks:Fire("AreaCreated", srcGUID, srcName, triggerGUID, spellid, spellschool, value, quality)
+end
+
+local Fire = callbacks.Fire
+function Core.HitUnit(guid, absorbedTotal, overkill, spellschool)
+ local guidEffects = activeEffectsBySpell[guid]
+ if not guidEffects then return end
+
+ local absorbedRemaining = absorbedTotal
+
+ local absorbed = 0
+ local keepEffect, visibleAbsorb = false, false
+ local i = 1
+ local effectEntry
+
+ -- This loop lasts as long as there is still an absorb value that
+ -- no effect could account for, but it will break once the list of
+ -- available effects got used completely.
+ while absorbedRemaining > 0 do
+ effectEntry = activeEffectsByPriority[guid][i]
+
+ -- Sometimes there can be holes in this list since we don't re-sort
+ -- after removing an effect
+ if effectEntry == nil then break end
+
+ -- Only absorb effects with exactly zero are ignored, negative ones
+ -- are treated as infinite (that is, no addon should display their value)
+ if effectEntry[3] ~= 0 then
+ -- Hit the abosrb effect
+ absorbed, keepEffect = Effects[effectEntry[1]][4](effectEntry, absorbedRemaining, overkill, spellschool)
+
+ if absorbed > 0 then
+ -- Reduce the value of this effect and the remaining absorb value
+ -- to be accounted for
+ effectEntry[3] = effectEntry[3] - absorbed
+ absorbedRemaining = absorbedRemaining - absorbed
+
+ if effectEntry[8] then
+ Fire(callbacks, "AreaUpdated", effectEntry[8], effectEntry[3])
+ else
+ -- If it should be visible (priority < 2), correct the total value
+ if effectEntry[2] < 2 then
+ guidEffects[-1] = guidEffects[-1] - absorbed
+ -- Shows us that at least one visible effect got hit
+ visibleAbsorb = true
+ end
+
+ Fire(callbacks, "EffectUpdated", guid, effectEntry[1], effectEntry[3])
+ end
+ end
+
+ -- If the hit-function told us to remove the effect, do so
+ -- Note that only RemoveActiveEffect is allowed to remove it from the
+ -- list, we just set the value to zero, so it gets ignored on any hit.
+ -- This is do not come into any desync issues with the events, and to
+ -- keep the proper clean-up code in one place
+ if not keepEffect then
+ effectEntry[3] = 0
+ end
+ end
+
+ i = i + 1
+ end
+
+ -- There are two possibilities when things are going wrong
+ --
+ -- a) we guessed an absorb value too high, in that case it will
+ -- automatically be corrected when it breaks
+ --
+ -- b) we guessed an absorb value too low, so we end up with an
+ -- amount to absorb when all effects seem to be gone
+ -- (absorbedRemaining > 0)
+ -- Note that we cannot rely on SPELL_AURA_REMOVED to check this,
+ -- since it may happen completely out of order, but it will
+ -- clear this unit soon or did so already.
+ -- we reduce the quality to zero, since any absorb now happening
+ -- cannot be accounted for.
+ -- Since we may have rounding errors from scanning the spellbook
+ -- and calculating the value thereafter (does Blizzard round on
+ -- EVERY step?!?), we accept a small threshold
+ if absorbedRemaining > LOW_VALUE_TOLERANCE then
+ guidEffects[-1] = guidEffects[-1] - absorbedRemaining
+ guidEffects[-2] = 0.0
+ visibleAbsorb = true
+ end
+
+ if visibleAbsorb then
+ Fire(Core, "UnitUpdated", guid, guidEffects[-1], guidEffects[-2])
+ Fire(Core, "UnitAbsorbed", guid, absorbedTotal)
+ end
+end
+
+-- Note that this method should NOT be called on non-existing effects or units
+-- There are no exist checks within it.
+function Core.RemoveActiveEffect(guid, spellid)
+ local guidEffects = activeEffectsBySpell and activeEffectsBySpell[guid]
+ if not guidEffects then return end
+
+ local effectEntry = guidEffects[spellid]
+
+ -- This is a shared effect with a triggerGUID
+ if effectEntry[8] then
+ effectEntry[9] = effectEntry[9] - 1
+
+ if guid ~= effectEntry[8] then
+ callbacks:Fire("EffectRemoved", guid, spellid)
+ end
+
+ if effectEntry[9] == 0 then
+ if effectEntry[6] then
+ Core:CancelTimer(effectEntry[6], true)
+ end
+ callbacks:Fire("AreaCleared", effectEntry[8])
+ end
+ else
+ if effectEntry[2] < 2 then
+ guidEffects[-1] = guidEffects[-1] - effectEntry[3]
+ callbacks:Fire("UnitUpdated", guid, guidEffects[-1], guidEffects[-2])
+ end
+
+ Core:CancelTimer(effectEntry[6], true)
+ callbacks:Fire("EffectRemoved", guid, spellid)
+ end
+
+ if #(activeEffectsByPriority[guid]) == 1 then
+ activeEffectsBySpell[guid] = nil
+ activeEffectsByPriority[guid] = nil
+ callbacks:Fire("UnitCleared", guid)
+ else
+ guidEffects[spellid] = nil
+ for k, v in pairs(activeEffectsByPriority[guid]) do
+ if v[1] == spellid then
+ tremove(activeEffectsByPriority[guid], k)
+ break
+ end
+ end
+ tsort(activeEffectsByPriority[guid], SortEffects)
+ end
+end
+
+-- This is uses a _very_ simple queue implementation with tinsert and tremove.
+-- It will not scale very well for large values, but we're talking of a maximum
+-- of ~3 entries per GUID at any given time. A proper implementation
+-- with linked list would probably not be any faster.
+function Core.PushCharge(guid, spellid, amount, lifetime)
+ local guidCharges = activeCharges[guid]
+
+ if not guidCharges then
+ activeCharges[guid] = {[spellid] = {lastCombatLogEvent + lifetime, amount}}
+ elseif not guidCharges[spellid] then
+ guidCharges[spellid] = {lastCombatLogEvent + lifetime, amount}
+ else
+ tinsert(guidCharges[spellid], lastCombatLogEvent + lifetime)
+ tinsert(guidCharges[spellid], amount)
+ end
+end
+
+function Core.PopCharge(guid, spellid)
+ local guidCharges = activeCharges[guid]
+
+ if guidCharges then
+ local queue = guidCharges[spellid]
+
+ -- For some weird reason, it will fail (true even on empty array)
+ -- if checked for queue[1] ?!?
+ if queue and queue[2] then
+ local chargeAmount
+ local chargeExpire
+
+ -- This loop will not be able to run infinitely
+ while true do
+ -- In this order we might save one table reshuffle
+ chargeAmount = tremove(queue, 2)
+ if not chargeAmount then return 0 end
+
+ chargeExpire = tremove(queue, 1)
+ if chargeExpire > lastCombatLogEvent then
+ return chargeAmount
+ end
+ end
+ end
+ end
+ return 0
+end
+
+function Core.AddCombatTrigger(target, event, func)
+ local eventTriggers = Core.CombatTriggers[event]
+ local oldTrigger = eventTriggers[target]
+
+ if not oldTrigger then
+ eventTriggers[target] = func
+ else
+ local listIndex = target .. "_list"
+ local funcList = eventTriggers[listIndex]
+
+ -- There is already a list of callbacks, so we just add this one
+ if funcList then
+ -- We used a direct call so far, create a list and set up a handler
+ for k, v in pairs(funcList) do
+ if v == func then
+ return
+ end
+ end
+ tinsert(funcList, func)
+ else
+ if oldTrigger == func then
+ return
+ end
+
+ funcList = {oldTrigger, func}
+ eventTriggers[listIndex] = funcList
+
+ local handler = function(...)
+ for k, v in pairs(funcList) do
+ v(...)
+ end
+ end
+
+ eventTriggers[target] = handler
+ end
+ end
+end
+
+function Core.RemoveCombatTrigger(target, event, func)
+ local eventTriggers = Core.CombatTriggers[event]
+
+ local listIndex = target .. "_list"
+ local funcList = eventTriggers[listIndex]
+
+ -- We have a list of callbacks, reduce if possible
+ if (funcList) then
+ -- It was a direct call anyway
+ if (#funcList == 2) then
+ eventTriggers[target] = (funcList[1] == func) and funcList[2] or funcList[1]
+ eventTriggers[listIndex] = nil
+ else
+ -- ATTENTION: We have to keep the table in place
+ -- because the handler references this table
+ local old_funcList = DeepTableCopy(funcList)
+ wipe(funcList)
+
+ for k, v in pairs(old_funcList) do
+ if v ~= func then
+ tinsert(funcList, v)
+ end
+ end
+ end
+ else
+ eventTriggers[target] = nil
+ end
+end
+
+local lastAP, lastSP = 0, 0
+function Core.SendUnitStats()
+ if curChatChannel then
+ local curAP, curSP = UnitStatsTable[playerid][2], UnitStatsTable[playerid][3]
+ if (curAP ~= lastAP) or (curSP ~= lastSP) then
+ Core:SendCommMessage(COMM_UNITSTATS, Core:Serialize(playerid, playerclass, curAP, curSP), curChatChannel)
+ Core:SendCommMessage(COMM_UNITSTATS_ALT, Core:Serialize(playerid, playerclass, curAP, curSP), curChatChannel)
+
+ lastAP, lastSP = curAP, curSP
+ CommStatsCooldown = true
+ Core:ScheduleTimer(ClearCommStatsCooldown, 15)
+ end
+ end
+end
+
+function Core.SendScaling()
+ if curChatChannel then
+ Core:SendCommMessage(COMM_SCALING, Core:Serialize(playerid, playerclass, Events.OnScalingEncode()), curChatChannel)
+ Core:SendCommMessage(COMM_SCALING_ALT, Core:Serialize(playerid, playerclass, Events.OnScalingEncode()), curChatChannel)
+
+ CommScalingCooldown = true
+ Core:ScheduleTimer(ClearCommStatsCooldown, 30)
+ end
+end
+
+-- An extension of AceTimer to schedule a one-shot timer that will not
+-- be scheduled twice if scheduled again before it's fired.
+local activeTimers = {}
+function Core:ScheduleUniqueTimer(id, callback, delay, arg)
+ if not activeTimers[id] then
+ self:ScheduleTimer(function()
+ callback(arg)
+ activeTimers[id] = nil
+ end, delay)
+ activeTimers[id] = 1
+ end
+end
+
+function Core.SetActive()
+ Core.RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ lib.Passive = false
+end
+
+function Core.SetPassive()
+ Core.UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ lib.Passive = true
+end
+
+function Core.SetVerbose()
+ Core.RegisterEvent("PARTY_MEMBERS_CHANGED")
+ Core.RegisterEvent("RAID_ROSTER_UPDATE")
+ lib.Silent = false
+ Events.GROUPING_CHANGED()
+end
+
+function Core.SetSilent()
+ Core.UnregisterEvent("PARTY_MEMBERS_CHANGED")
+ Core.UnregisterEvent("RAID_ROSTER_UPDATE")
+ lib.Silent = true
+ curChatChannel = nil
+end
+
+ApplySingularEffect = Core.ApplySingularEffect
+ApplyAreaEffect = Core.ApplyAreaEffect
+CreateAreaTrigger = Core.CreateAreaTrigger
+HitUnit = Core.HitUnit
+RemoveActiveEffect = Core.RemoveActiveEffect
+
+---------------------
+-- Event functions --
+---------------------
+
+function Events.PLAYER_ENTERING_WORLD()
+ if not GetTalentInfo(1, 1) then
+ Core.RegisterEvent("PLAYER_ALIVE")
+ else
+ Core.Available = true
+ Core.Enable()
+ Core:ScheduleTimer(Events.STATS_CHANGED, 5)
+ end
+ Core.UnregisterEvent("PLAYER_ENTERING_WORLD")
+end
+
+function Events.PLAYER_ALIVE()
+ Core.Available = true
+ Core.Enable()
+ Core.UnregisterEvent("PLAYER_ALIVE")
+end
+
+local BITMASK_GROUP = COMBATLOG_OBJECT_AFFILIATION_MINE + COMBATLOG_OBJECT_AFFILIATION_PARTY + COMBATLOG_OBJECT_AFFILIATION_RAID
+local function CheckFlags(srcFlags, dstFlags)
+ return (srcFlags and bit.band(srcFlags, BITMASK_GROUP) ~= 0) or (dstFlags and bit.band(dstFlags, BITMASK_GROUP) ~= 0)
+end
+
+------------------
+-- Heal Absorbs --
+------------------
+
+local absorbHealSpells = {
+ [66237] = 30000, -- Incinerate Flesh (10 Normal)
+ [67049] = 60000, -- Incinerate Flesh (25 Normal)
+ [67050] = 40000, -- Incinerate Flesh (10 Heroic)
+ [67051] = 85000, -- Incinerate Flesh (25 Heroic)
+ [66236] = 30000, -- Incinerate Flesh
+ [70659] = 9000, -- Necrotic Strike (10 Normal)
+ [71951] = 15000, -- Necrotic Strike
+ [72490] = 14000, -- Necrotic Strike (25 Normal)
+ [72491] = 14000, -- Necrotic Strike (10 Heroic)
+ [72492] = 20000, -- Necrotic Strike (25 Heroic)
+}
+
+local environmentSchools = {
+ FALLING = SCHOOL_MASK_PHYSICAL,
+ DROWNING = SCHOOL_MASK_PHYSICAL,
+ FATIGUE = SCHOOL_MASK_PHYSICAL,
+ FIRE = SCHOOL_MASK_FIRE,
+ LAVA = SCHOOL_MASK_FIRE,
+ SLIME = SCHOOL_MASK_NATURE
+}
+
+function Events.COMBAT_LOG_EVENT_UNFILTERED(timestamp, etype, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, ...)
+ if lib.CheckFlags and not CheckFlags(srcFlags, dstFlags) then return end
+
+ lastCombatLogEvent = timestamp
+ if etype == "SWING_DAMAGE" then
+ local amount, _, _, _, _, absorbed = ...
+ if not absorbed then return end
+ HitUnit(dstGUID, absorbed, amount, SCHOOL_MASK_PHYSICAL)
+ elseif etype == "RANGE_DAMAGE" or etype == "SPELL_DAMAGE" or etype == "SPELL_PERIODIC_DAMAGE" then
+ local _, _, spellschool, amount, _, _, _, _, absorbed = ...
+ if not absorbed then return end
+ HitUnit(dstGUID, absorbed, amount, spellschool)
+ elseif etype == "ENVIRONMENTAL_DAMAGE" then
+ local envtype, amount, _, _, _, _, absorbed = ...
+ if not absorbed then return end
+ HitUnit(dstGUID, absorbed, amount, environmentSchools[envtype] or SCHOOL_MASK_PHYSICAL)
+ elseif etype == "SWING_MISSED" then
+ local misstype, amount = ...
+ if misstype ~= "ABSORB" then return end
+ HitUnit(dstGUID, amount, 0, SCHOOL_MASK_PHYSICAL)
+ elseif etype == "RANGE_MISSED" or etype == "SPELL_MISSED" or etype == "SPELL_PERIODIC_MISSED" then
+ local _, _, spellschool, misstype, amount = ...
+ if misstype ~= "ABSORB" then return end
+ HitUnit(dstGUID, amount, 0, spellschool)
+ elseif etype == "SPELL_HEAL" or etype == "SPELL_PERIODIC_HEAL" then
+ local spellid, _, _, amount, overheal, absorbed, critical = ...
+ if CombatTriggersOnHeal[srcGUID] then
+ CombatTriggersOnHeal[srcGUID](srcGUID, srcName, dstGUID, dstName, spellid, amount, overheal)
+ end
+ if absorbed and Core.GUIDtoAbsorbHealSpells[dstGUID] then
+ Core.GUIDtoAbsorbHealSpells[dstGUID] = Core.GUIDtoAbsorbHealSpells[dstGUID] - absorbed
+ end
+ if critical and CombatTriggersOnHealCrit[srcGUID] then
+ CombatTriggersOnHealCrit[srcGUID](srcGUID, srcName, dstGUID, dstName, spellid, amount, overheal)
+ end
+ elseif etype == "SPELL_AURA_APPLIED" or etype == "SPELL_AURA_REFRESH" then
+ local spellid, _, spellschool = ...
+ if Effects[spellid] then
+ if Effects[spellid][1] > 0 then
+ ApplySingularEffect(timestamp, srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ else
+ ApplyAreaEffect(timestamp, srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ end
+ end
+ if CombatTriggersOnAuraApplied[spellid] then
+ CombatTriggersOnAuraApplied[spellid](srcGUID, srcName, dstGUID, dstName, spellid)
+ end
+ if absorbHealSpells[spellid] then
+ Core.GUIDtoAbsorbHealSpells[dstGUID] = absorbHealSpells[spellid]
+ end
+ elseif etype == "SPELL_AURA_REMOVED" then
+ local spellid, _, spellschool = ...
+ if Effects[spellid] then
+ if activeEffectsBySpell[dstGUID] and activeEffectsBySpell[dstGUID][spellid] then
+ RemoveActiveEffect(dstGUID, spellid)
+ end
+ end
+ if CombatTriggersOnAuraRemoved[spellid] then
+ CombatTriggersOnAuraRemoved[spellid](srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ end
+ if absorbHealSpells[spellid] then
+ Core.GUIDtoAbsorbHealSpells[dstGUID] = nil
+ end
+ elseif etype == "SPELL_SUMMON" then
+ local spellid, _, spellschool = ...
+ if AreaTriggers[spellid] then
+ CreateAreaTrigger(timestamp, srcGUID, srcName, dstGUID, dstName, AreaTriggers[spellid], spellschool)
+ end
+ end
+end
+
+function Events.GROUPING_CHANGED()
+ -- Note that the order here is VERY important
+ if UnitInBattleground("player") then
+ curChatChannel = "BATTLEGROUND"
+ groupCount = GetNumRaidMembers()
+ elseif GetNumRaidMembers() > 0 then
+ curChatChannel = "RAID"
+ groupCount = GetNumRaidMembers()
+ elseif GetNumPartyMembers() > 0 then
+ curChatChannel = "PARTY"
+ groupCount = GetNumPartyMembers()
+ else
+ curChatChannel = nil
+ groupCount = 0
+ end
+
+ Core:ScheduleTimer(Events.STATS_CHANGED, 5)
+ Events.STATS_CHANGED()
+end
+
+function Events.ZONE_CHANGED_NEW_AREA()
+ if UnitInBattleground("player") then
+ ZONE_MODIFIER = 1.17
+ elseif IsActiveBattlefieldArena() then
+ ZONE_MODIFIER = 0.9
+ elseif GetCurrentMapAreaID() == 502 then
+ ZONE_MODIFIER = 1.17
+ elseif GetCurrentMapAreaID() == 605 then
+ ZONE_MODIFIER = (UnitBuff("player", GetSpellInfo(73822)) or UnitBuff("player", GetSpellInfo(73828))) and 1.3 or 1
+ else
+ ZONE_MODIFIER = 1
+ end
+
+ Core:ScheduleTimer(Events.STATS_CHANGED, 5)
+end
+
+function Events.STATS_CHANGED()
+ local baseAP, plusAP, minusAP = UnitAttackPower("player")
+
+ UnitStatsTable[playerid][2] = baseAP + plusAP - minusAP
+ -- TODO: What about spell power ~= healing spell power?
+ UnitStatsTable[playerid][3] = GetSpellBonusHealing()
+ if curChatChannel then
+ Core:ScheduleUniqueTimer("comm_stats", Core.SendUnitStats, CommStatsCooldown and 15 or 5)
+ Core:ScheduleUniqueTimer("comm_scaling", Core.SendScaling, CommScalingCooldown and 30 or 5)
+ end
+end
+
+function Events.OnUnitStatsReceived(prefix, text, distribution, target)
+ if not text then return end
+
+ local success, guid, class, ap, sp = Core:Deserialize(text)
+ if not (success and guid and class and ap and sp) then return end
+ if guid == playerid then return end
+
+ if not UnitStatsTable[guid] then
+ UnitStatsTable[guid] = {class, ap, sp, 1.0}
+ else
+ UnitStatsTable[guid][2] = ap
+ UnitStatsTable[guid][3] = sp
+ UnitStatsTable[guid][4] = 1.0
+ end
+end
+
+function Events.OnScalingEncode()
+ return Scaling[playerid]
+end
+
+function OnScalingDecode.DEFAULT(guid, guidScaling)
+ Scaling[guid] = guidScaling
+end
+
+function Events.OnScalingReceived(prefix, text, distribution, target)
+ if not text then return end
+
+ local success, guid, class, inScaling = Core:Deserialize(text)
+ if not (success and guid and class and inScaling) then return end
+ if guid == playerid then return end
+ (OnScalingDecode[class])(guid, inScaling)
+end
+
+function Events.OnPeriodicBroadcast()
+ if not curChatChannel then return end
+
+ local playerStats = UnitStatsTable[playerid]
+ local playerScaling = Scaling[playerid]
+
+ if not CommStatsCooldown then
+ Core:ScheduleUniqueTimer("comm_stats", Core.SendUnitStats, 5)
+ end
+
+ if not CommScalingCooldown then
+ Core:ScheduleUniqueTimer("comm_scaling", Core.SendScaling, 5)
+ end
+end
+
+function Events.OnSingularTimeout(args)
+ local guid, spellid = args[1], args[2]
+ if activeEffectsBySpell[guid] and activeEffectsBySpell[guid][spellid] then
+ RemoveActiveEffect(guid, spellid)
+ end
+end
+
+function Events.OnSingularActivityCheck(args)
+ local guid, spellid = args[1], args[2]
+
+ if activeEffectsBySpell[guid] and activeEffectsBySpell[guid][spellid] then
+ local name = select(6, GetPlayerInfoByGUID(guid))
+
+ -- We cannot track whether it's still on, remove it
+ if not name then
+ Core:CancelTimer(activeEffectsBySpell[guid][spellid][6])
+ RemoveActiveEffect(guid, spellid)
+ end
+
+ local stillActive = false
+
+ for i = 1, 40 do
+ local buffId = select(11, UnitBuff(name, i))
+ if not buffId then break end
+ if buffId == spellid then
+ stillActive = true
+ break
+ end
+ end
+
+ if not stillActive then
+ Core:CancelTimer(activeEffectsBySpell[guid][spellid][6])
+ RemoveActiveEffect(guid, spellid)
+ end
+ else
+ -- Make sure to remove the timer
+ Core:CancelTimer(activeEffectsBySpell[guid][spellid][6])
+ end
+end
+
+-- This should be called in even less cases than the normal timeout
+function Events.OnAreaTimeout(areaEntry)
+ -- Disable the timer handle entry already
+ areaEntry[6] = nil
+
+ for guid, guidEffects in pairs(activeEffectsBySpell) do
+ if areaEntry[9] == 0 then return end
+ for spellid, effectEntry in pairs(guidEffects) do
+ if effectEntry == areaEntry then
+ RemoveActiveEffect(guid, spellid)
+ end
+ end
+ end
+
+ -- We're only here if we didn't reduce the refcount to zero
+ Core.Error("Positive refcount " .. areaEntry[9] .. " remained for area effect " .. areaEntry[1] .. " by trigger " .. areaEntry[8])
+end
+
+-- Map client events to our callbacks
+Events.PARTY_MEMBERS_CHANGED = Events.GROUPING_CHANGED
+Events.RAID_ROSTER_UPDATE = Events.GROUPING_CHANGED
+Events.PLAYER_DAMAGE_DONE_MODS = Events.STATS_CHANGED
+Events.UNIT_ATTACK_POWER = Events.STATS_CHANGED
+
+----------------------
+-- Public functions --
+----------------------
+
+function lib.RegisterEffectCallbacks(self, funcApplied, funcUpdated, funcRemoved)
+ lib.RegisterCallback(self, "EffectApplied", funcApplied)
+ lib.RegisterCallback(self, "EffectUpdated", funcUpdated or funcApplied)
+ lib.RegisterCallback(self, "EffectRemoved", funcRemoved or funcApplied)
+end
+
+function lib.RegisterUnitCallbacks(self, funcUpdated, funcCleared)
+ lib.RegisterCallback(self, "UnitUpdated", funcUpdated)
+ lib.RegisterCallback(self, "UnitCleared", funcCleared or funcUpdated)
+end
+
+function lib.RegisterAreaCallbacks(self, funcCreated, funcUpdated, funcCleared)
+ lib.RegisterCallback(self, "AreaCreated", funcCreated)
+ lib.RegisterCallback(self, "AreaUpdated", funcUpdated or funcCreated)
+ lib.RegisterCallback(self, "AreaCleared", funcCleared or funcCreated)
+end
+
+function lib.GetLowValueTolerance()
+ return LOW_VALUE_TOLERANE
+end
+
+function lib.SetLowValueTolerance(value)
+ LOW_VALUE_TOLERANCE = tonumber(value)
+end
+
+function lib.PrintProfiling()
+ if GetCVar("scriptProfile") ~= "1" then
+ Core.Print("CPU profiling disabled")
+ return
+ end
+
+ UpdateAddOnCPUUsage()
+ UpdateAddOnMemoryUsage()
+
+ Core.Print("Mem: " .. format("%.3f", GetAddOnMemoryUsage("SpecializedAbsorbs")) .. " kB")
+ Core.Print("Time: " .. format("%.3f", GetAddOnCPUUsage("SpecializedAbsorbs")) .. " ms")
+ Core.Print("--- critical code paths (all times in ms) ---")
+
+ local funcTable
+
+ funcTable = {
+ ["ApplySingularEffect"] = ApplySingularEffect,
+ ["HitUnit"] = HitUnit,
+ ["RemoveActiveEffect"] = RemoveActiveEffect,
+ ["OnCombatLogEvent"] = COMBAT_LOG_EVENT_UNFILTERED,
+ ["SortEffects"] = SortEffects
+ }
+
+ local v_type
+ local time_self, time_combined, count
+
+ for k, v in pairs(funcTable) do
+ v_type = type(v)
+
+ if v_type == "function" then
+ time_self, count = GetFunctionCPUUsage(v, false)
+ time_combined = GetFunctionCPUUsage(v, true)
+ Core.Print(k .. " (#" .. count .. "): " .. format("%.4f", time_self) .. " / " .. format("%.4f", time_combined))
+ end
+ end
+end
+
+function lib.UnitTotal(guid)
+ local total = 0
+ if activeEffectsBySpell and activeEffectsBySpell[guid] then
+ total = activeEffectsBySpell[guid][-1]
+ end
+ return total
+end
+
+function lib.UnitEffect(guid, spellid)
+ local guidEffects = activeEffectsBySpell and activeEffectsBySpell[guid]
+ if guidEffects and guidEffects[spellid] then
+ return guidEffects[spellid][3]
+ end
+ return 0
+end
+
+function lib.UnitStats(guid, missingQuality)
+ local guidStats = UnitStatsTable and UnitStatsTable[guid]
+ if guidStats then
+ return guidStats[2], guidStats[3], guidStats[4]
+ end
+ return 0, 0, missingQuality
+end
+
+function lib.UnitScaling(guid, defaultScaling, defaultQuality)
+ local guidScaling = Scaling and Scaling[guid]
+ if guidScaling then
+ return guidScaling, 1.0
+ end
+ return defaultScaling, defaultQuality
+end
+
+-- Optimized method to save one function call on creation, since a lot of spells
+-- actually require stats and scaling
+function lib.UnitStatsAndScaling(guid, missingQuality, defaultScaling, defaultQuality)
+ local guidStats = UnitStatsTable and UnitStatsTable[guid]
+ local guidScaling = Scaling and Scaling[guid]
+ if guidStats then
+ if guidScaling then
+ return guidStats[2], guidStats[3], guidStats[4], guidScaling, 1.0
+ end
+ return guidStats[2], guidStats[3], guidStats[4], defaultScaling, defaultQuality
+ else
+ if guidScaling then
+ return 0, 0, missingQuality, guidScaling, 1.0
+ end
+ return 0, 0, missingQuality, defaultScaling, defaultQuality
+ end
+end
+
+function lib.UnitEffectsMap(guid)
+ return activeEffectsBySpell and activeEffectsBySpell[guid]
+end
+
+function lib.UnitEffectsList(guid)
+ return activeEffectsByPriority and activeEffectsByPriority[guid]
+end
+
+function lib.UnitActiveEffect(guid)
+ local guidEffects = lib.UnitEffectsList(guid)
+ return guidEffects and guidEffects[1]
+end
+
+function lib.ScheduleScalingBroadcast()
+ if curChatChannel then
+ Core:ScheduleUniqueTimer("comm_scaling", Core.SendScaling, CommScalingCooldown and 30 or 5)
+ end
+end
+
+function lib.UnitTotalHealAbsorbs(guid)
+ if GUIDtoAbsorbHealSpells then
+ local guidHealAbsorbs = guid and GUIDtoAbsorbHealSpells[guid]
+ return guidHealAbsorbs
+ end
+end
+
+------------------------------
+-- Generic Effect functions --
+------------------------------
+
+local PushCharge = Core.PushCharge
+local PopCharge = Core.PopCharge
+local UnitStats = lib.UnitStats
+local UnitScaling = lib.UnitScaling
+local UnitStatsAndScaling = lib.UnitStatsAndScaling
+
+--- Generic Create function (only for documentary purposes)
+-- @param srcGUID guid of the originating unit
+-- @param srcName name of the originating unit
+-- @param dstGUID guid of the affected unit
+-- @param dstName name of the affected unit
+-- @param spellid the spellid of this absorb effect
+-- @param destEffects activeEffectsBySpell[dstGUID]
+-- @return total value of this absorb effect
+-- @return quality (that is accuracy) of this value
+local function generic_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+end
+
+-- Generic Create function for constant effects pulled from a table
+-- Expects the table at effect[5] indexed by spellid with base values
+local function generic_ConstantByTable_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ return Effects[spellid][5][spellid], 1.0
+end
+
+-- Generic Create function for effects simply scaling with spellpower and a fixed
+-- coefficient.
+-- Expects at effect[5] a table indexed by spellid with the base values and at
+-- effect[6] the spellpower coefficient
+local function generic_SpellScalingByTable_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local effectInfo = Effects[spellid]
+ local _, sp, quality = UnitStats(srcGUID, 0.1)
+ return floor(effectInfo[5][spellid] + (sp * effectInfo[6])), quality
+end
+
+--- Generic Hit function suitable for most absorb effects
+-- Note that this function is only responsible for determining the amount this
+-- particular absorb effect will take, NOT to handle its consequences like updating
+-- the data structures
+-- @param effectEntry activeEffectsBySpell[guid][spellid]
+-- @param absorbedRemaining absorb value left to be accounted for on this unit
+-- @param overkill amount of damage done on top of the absorb
+-- @param spellschool spell school for this hit
+-- @return absorb value this effect can account fors
+-- @return whether this absorb was broken by this hit
+local function generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if absorbedRemaining > effectEntry[3] then
+ -- dirty but efficient
+ absorbedRemaining = effectEntry[3]
+ overkill = 1
+ end
+ return absorbedRemaining, (overkill == 0)
+end
+
+local function Physical_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_PHYSICAL then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Holy_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_HOLY then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Fire_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FIRE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Nature_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_NATURE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Frost_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FROST then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function FrostFire_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= (SCHOOL_MASK_FIRE + SCHOOL_MASK_FROST) then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Shadow_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_SHADOW then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Arcane_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_ARCANE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+---------------------------
+-- Effects: Death Knight --
+---------------------------
+
+local deathknight_MS_Ranks = {[0] = 0, [1] = 0.08, [2] = 0.16, [3] = 0.25}
+
+-- Public Scaling: { [MagicSuppression] }
+local deathknight_defaultScaling = {0}
+
+local function deathknight_AntiMagicShell_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local maxHealth = UnitDispatch(UnitHealthMax, dstGUID, dstName)
+ if maxHealth == 0 then
+ return 0, 0.0
+ end
+
+ local sourceScaling, quality = UnitScaling(srcGUID, deathknight_defaultScaling, 0.5)
+ return floor(maxHealth * 0.5), quality, (0.75 + sourceScaling[1])
+end
+
+local function deathknight_AntiMagicShell_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ -- TODO: what happens to mixed school attacks?
+ if spellschool == SCHOOL_MASK_PHYSICAL then
+ return 0, true
+ end
+
+ local maxAbsorb = floor((absorbedRemaining + overkill) * effectEntry[7])
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_AntiMagicZone_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local ap, _, quality = UnitStats(srcGUID, 0.3)
+ return (10000 + 2 * ap), quality
+end
+
+local function deathknight_AntiMagicZone_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ -- TODO: what happens to mixed school attacks?
+ if spellschool == SCHOOL_MASK_PHYSICAL then
+ return 0, true
+ end
+
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.75)
+
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_WoN_Hit1(effectEntry, absorbedRemaining, overkill, spellschool)
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.05)
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_WoN_Hit2(effectEntry, absorbedRemaining, overkill, spellschool)
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.1)
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_WoN_Hit3(effectEntry, absorbedRemaining, overkill, spellschool)
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.15)
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_OnTalentUpdate()
+ -- Magic Suppression
+ local t = select(5, GetTalentInfo(3, 18))
+ playerScaling[1] = deathknight_MS_Ranks[t]
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.DEATHKNIGHT()
+ Events.PLAYER_TALENT_UPDATE = deathknight_OnTalentUpdate
+ deathknight_OnTalentUpdate()
+end
+
+--------------------
+-- Effects: Druid --
+--------------------
+
+local function druid_SavageDefense_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local ap, _, quality = UnitStats(srcGUID, 0.0)
+ return floor(ap * 0.25), quality
+end
+
+local function druid_SavageDefense_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ -- TODO: what happens to mixed school attacks?
+ if spellschool == SCHOOL_MASK_PHYSICAL then
+ return min(effectEntry[3], absorbedRemaining), false
+ end
+ return 0, true
+end
+
+-------------------
+-- Effects: Mage --
+-------------------
+
+-- Table for base values of
+-- Fire Ward, Frost Ward, Ice Barrier, Mana Shield
+-- TODO: base leveling increase
+local mage_Absorb_Spells = {
+ -- Fire Ward
+ [543] = 165,
+ [8457] = 290,
+ [8458] = 470,
+ [10223] = 675,
+ [10225] = 875,
+ [27218] = 1125,
+ [43010] = 1950,
+ -- Frost Ward
+ [6143] = 165,
+ [8461] = 290,
+ [8462] = 470,
+ [10177] = 675,
+ [28609] = 875,
+ [32796] = 1125,
+ [43012] = 1950,
+ -- Ice Barrier
+ [11426] = 438,
+ [13031] = 549,
+ [13032] = 678,
+ [13033] = 818,
+ [27134] = 925,
+ [33405] = 1075,
+ [43038] = 2800,
+ [43039] = 3300,
+ -- Mana Shield
+ [1463] = 120,
+ [8494] = 210,
+ [8495] = 300,
+ [10191] = 390,
+ [10192] = 480,
+ [10193] = 570,
+ [27131] = 715,
+ [43019] = 1080,
+ [43020] = 1330
+}
+
+-- Public Scaling: { [GlyphOfIceBarrier] }
+local mage_defaultScaling = {1.0}
+
+-- No Downranking support here
+local function mage_IceBarrier_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local _, sp, quality1, sourceScaling, quality2 = UnitStatsAndScaling(srcGUID, 0.3, mage_defaultScaling, 0.4)
+ return floor((mage_Absorb_Spells[spellid] + (sp * 0.8053)) * sourceScaling[1]), min(quality1, quality2)
+end
+
+local function mage_FireWard_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FIRE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function mage_FrostWard_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FROST then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function mage_OnGlyphUpdated()
+ local glyphSpellId
+
+ playerScaling[1] = 1.0
+
+ for i = 1, 6 do
+ glyphSpellId = select(3, GetGlyphSocketInfo(i))
+ -- Glyph of Ice Barrier
+ if glyphSpellId and glyphSpellId == 63095 then
+ playerScaling[1] = 1.3
+ break
+ end
+ end
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.MAGE()
+ Events.GLYPH_UPDATED = mage_OnGlyphUpdated
+ mage_OnGlyphUpdated()
+end
+
+----------------------
+-- Effects: Paladin --
+----------------------
+
+-- Public Scaling: { [DivineGuardian] }
+local paladin_defaultScaling = {1.0}
+
+-- The base value is always 500
+local function paladin_SacredShield_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local _, sp, quality1, sourceScaling, quality2 = UnitStatsAndScaling(srcGUID, 0.1, paladin_defaultScaling, 0.2)
+ return floor((500 + (sp * 0.75)) * (sourceScaling[1] or paladin_defaultScaling[1]) * ZONE_MODIFIER), min(quality1, quality2)
+end
+
+local function paladin_OnTalentUpdate()
+ -- No need to do it before
+ if UnitLevel("player") < 80 then return end
+
+ -- Divine Guardian
+ local t = select(5, GetTalentInfo(2, 9))
+ playerScaling[1] = 1 + (t * 0.1)
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.PALADIN()
+ Events.PLAYER_TALENT_UPDATE = paladin_OnTalentUpdate
+ paladin_OnTalentUpdate()
+end
+
+---------------------
+-- Effects: Priest --
+---------------------
+
+PRIEST_DIVINEAEGIS_SPELLID = 47753
+
+-- [rank] = {spellid, level, baseValue, incValue}
+local priest_PWS_Ranks = {
+ [1] = {17, 6, 44, 4},
+ [2] = {592, 12, 88, 6},
+ [3] = {600, 18, 158, 8},
+ [4] = {3747, 24, 234, 10},
+ [5] = {6065, 30, 301, 11},
+ [6] = {6066, 36, 381, 13},
+ [7] = {10898, 42, 484, 15},
+ [8] = {10899, 48, 605, 17},
+ [9] = {10900, 54, 763, 19},
+ [10] = {10901, 60, 942, 21},
+ [11] = {25217, 65, 1125, 18},
+ [12] = {25218, 70, 1265, 20},
+ [13] = {48065, 75, 1920, 30},
+ [14] = {48066, 80, 2230, 0}
+}
+
+-- Public Scaling:
+-- Power Word: Shield: [spellid] = {base, spFactor}
+-- Divine Aegis: [47753] = healFactor
+local priest_defaultScaling = {[PRIEST_DIVINEAEGIS_SPELLID] = 0}
+do
+ for k, v in pairs(priest_PWS_Ranks) do
+ priest_defaultScaling[v[1]] = {v[3], 0.809}
+ end
+end
+
+-- Private Scaling
+-- Talents: "TwinDisc", "ImpPWS", "FocusedPower", "DivineAegis", "BorrowedTime", "SpiritualHealing"
+-- Gear: "4pcRaid9", "4pcRaid10"
+-- Computed: base, sp, DA
+
+local function priest_PowerWordShield_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local _, sp, quality1, sourceScaling, quality2 = UnitStatsAndScaling(srcGUID, 0.1, priest_defaultScaling, 0.1)
+ sourceScaling[spellid] = sourceScaling[spellid] or priest_defaultScaling[spellid]
+ if sourceScaling[spellid] then
+ return floor((sourceScaling[spellid][1] + sp * sourceScaling[spellid][2]) * ZONE_MODIFIER), min(quality1, quality2)
+ end
+end
+
+local function priest_DivineAegis_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local existing = 0
+
+ if destEffects and destEffects[spellid] then
+ existing = destEffects[spellid][3]
+ end
+
+ local charge = PopCharge(dstGUID, spellid)
+
+ if charge == 0 then
+ return existing, 0.0
+ end
+
+ local destLevel = UnitDispatch(UnitLevel, dstGUID, dstName)
+ local quality = 1.0
+
+ if destLevel == 0 then
+ destLevel = 80
+ quality = 0.4
+ end
+
+ return min(destLevel * 125, existing + charge), quality
+end
+
+-- I officially HATE Divine Aegis (and Val'anyr for that matter) from now.
+-- After extensive testing and parsing/filtering a few hours of combat log, I found the following facts:
+-- * In MOST cases, every critical heal will trigger an AURA_APPLIED/AURA_REFRESHED event, even on multiple crits on
+-- one penance and with both AURA events being triggered after all critical heals. In rare cases, there is only one...
+-- * If on your side the aura get removed before the next one is applied (double penance crit, both critical heals
+-- are first in your combat log), then AURA_APPLIED -> AURA_REMOVED by damage -> AURA_APPLIED for 2nd crit
+-- * There are occasions where 2 Discipline priests apply two Divine Aegis auras. In general, the source of Divine Aegis
+-- is completely fucked up in those cases. If two priests channel penance at the same time, both crit, you can never say
+-- who will get the AURA_APPLIED event credited (yes, I found both the first-hitting priest as well as the second-hitting
+-- priest there!) and who the following AURA_REFRESHED events
+local function priest_DivineAegis_OnHealCrit(srcGUID, srcName, dstGUID, dstName, spellid, amount)
+ -- We can do a direct access to Scaling here, since the callback is only in place if it was present
+ -- in the first place
+ PushCharge(dstGUID, PRIEST_DIVINEAEGIS_SPELLID, floor(amount * Scaling[srcGUID][PRIEST_DIVINEAEGIS_SPELLID]), 5.0)
+end
+
+local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
+ local guidScaling
+
+ if not Scaling[guid] then
+ guidScaling = {}
+ Scaling[guid] = guidScaling
+ else
+ guidScaling = Scaling[guid]
+ end
+
+ guidScaling[PRIEST_DIVINEAEGIS_SPELLID] = daFactor
+
+ if daFactor > 0 then
+ Core.AddCombatTrigger(guid, "OnHealCrit", priest_DivineAegis_OnHealCrit)
+ else
+ Core.RemoveCombatTrigger(guid, "OnHealCrit", priest_DivineAegis_OnHealCrit)
+ end
+
+ local rankValue, rankSP
+
+ for k, v in pairs(priest_PWS_Ranks) do
+ if v[2] <= level then
+ if level == 80 then
+ rankValue = v[3] + v[4]
+ else
+ -- TODO
+ rankValue = v[3]
+ end
+
+ -- Based on the assumption that the decrease in sp coefficient is linear,
+ -- only tested for level 80 though so far
+ -- Cataclysm will help us get rid of this crap again
+ if v[2] < (level - 5) then
+ if level == 80 then
+ rankSP = max(spFactor * ((v[2] * 0.045228921) - 2.381389768), 0)
+ else
+ rankSP = 0
+ end
+ else
+ rankSP = spFactor
+ end
+
+ guidScaling[v[1]] = {rankValue * baseFactor, rankSP}
+ end
+ end
+end
+
+local function priest_UpdatePlayerScaling()
+ privateScaling.base = ((1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05)))
+
+ local spFactor = 0.807
+ spFactor = spFactor + (privateScaling["BorrowedTime"] * 0.08)
+ spFactor = spFactor * (1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05))
+ privateScaling.sp = spFactor
+ privateScaling.DA = (privateScaling["DivineAegis"] * 0.1) * (1 + (privateScaling["4pcRaid9"] * 0.03))
+ priest_ApplyScaling(playerid, UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA)
+ lib.ScheduleScalingBroadcast()
+end
+
+local function priest_ScanTalents()
+ -- Twin Disciplines
+ local t = select(5, GetTalentInfo(1, 2))
+ privateScaling["TwinDisc"] = t
+
+ -- Improved Power Word: Shield
+ t = select(5, GetTalentInfo(1, 9))
+ privateScaling["ImpPWS"] = t
+
+ -- Focused Power
+ t = select(5, GetTalentInfo(1, 16))
+ privateScaling["FocusedPower"] = t
+
+ -- Divine Aegis
+ t = select(5, GetTalentInfo(1, 24))
+ privateScaling["DivineAegis"] = t
+
+ -- Borrowed Time
+ t = select(5, GetTalentInfo(1, 27))
+ privateScaling["BorrowedTime"] = t
+
+ -- Spiritual Healing
+ t = select(5, GetTalentInfo(2, 16))
+ privateScaling["SpiritualHealing"] = t
+end
+
+local function priest_ScanEquipment()
+ local n = 0
+
+ -- Crimson Acolyte Raiment's 4-piece Bonus
+ if IsEquippedItem(50765) or IsEquippedItem(51178) or IsEquippedItem(51261) then
+ n = 1
+ end
+ if IsEquippedItem(50767) or IsEquippedItem(51175) or IsEquippedItem(51264) then
+ n = n + 1
+ end
+ if IsEquippedItem(50768) or IsEquippedItem(51176) or IsEquippedItem(51263) then
+ n = n + 1
+ end
+ if IsEquippedItem(50766) or IsEquippedItem(51179) or IsEquippedItem(51260) then
+ n = n + 1
+ end
+ if IsEquippedItem(50769) or IsEquippedItem(51177) or IsEquippedItem(51262) then
+ n = n + 1
+ end
+
+ if n >= 4 then
+ privateScaling["4pcRaid10"] = 1
+
+ -- no way to have 4pcRaid9 now
+ privateScaling["4pcRaid9"] = 0
+ return
+ else
+ privateScaling["4pcRaid10"] = 0
+ end
+
+ n = 0
+
+ -- Velen's/Zabra's Raiment 4-piece Bonus
+
+ -- UnitFactionGroup's first return value is NOT localized
+ if UnitFactionGroup("player") == "Alliance" then
+ if IsEquippedItem(47914) or IsEquippedItem(47984) or IsEquippedItem(48035) then
+ n = 1
+ end
+ if IsEquippedItem(47981) or IsEquippedItem(47987) or IsEquippedItem(48029) then
+ n = n + 1
+ end
+ if IsEquippedItem(47936) or IsEquippedItem(47986) or IsEquippedItem(48031) then
+ n = n + 1
+ end
+ if IsEquippedItem(47982) or IsEquippedItem(47983) or IsEquippedItem(48037) then
+ n = n + 1
+ end
+ if IsEquippedItem(47980) or IsEquippedItem(47985) or IsEquippedItem(48033) then
+ n = n + 1
+ end
+ else
+ if IsEquippedItem(48068) or IsEquippedItem(48065) or IsEquippedItem(48058) then
+ n = 1
+ end
+ if IsEquippedItem(48071) or IsEquippedItem(48062) or IsEquippedItem(48061) then
+ n = n + 1
+ end
+ if IsEquippedItem(48070) or IsEquippedItem(48063) or IsEquippedItem(48060) then
+ n = n + 1
+ end
+ if IsEquippedItem(48067) or IsEquippedItem(48066) or IsEquippedItem(48057) then
+ n = n + 1
+ end
+ if IsEquippedItem(48069) or IsEquippedItem(48064) or IsEquippedItem(48059) then
+ n = n + 1
+ end
+ end
+
+ if n >= 4 then
+ privateScaling["4pcRaid9"] = 1
+ else
+ privateScaling["4pcRaid9"] = 0
+ end
+end
+
+local function priest_OnLevelUp()
+ priest_UpdatePlayerScaling()
+end
+
+local function priest_OnTalentUpdate()
+ priest_ScanTalents()
+ priest_UpdatePlayerScaling()
+end
+
+local function priest_OnEquipmentChangedDelayed()
+ priest_ScanEquipment()
+ priest_UpdatePlayerScaling()
+end
+
+local function priest_OnEquipmentChanged()
+ Core:ScheduleUniqueTimer("priest_equip", priest_OnEquipmentChangedDelayed, 0.7)
+end
+
+local function priest_OnScalingEncode()
+ return {UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA}
+end
+
+function OnScalingDecode.PRIEST(guid, in_guidScaling)
+ if #in_guidScaling ~= 4 then return end
+ priest_ApplyScaling(guid, unpack(in_guidScaling))
+end
+
+function OnEnableClass.PRIEST()
+ Events.PLAYER_LEVEL_UP = priest_OnLevelUp
+ Events.PLAYER_TALENT_UPDATE = priest_OnTalentUpdate
+ Events.PLAYER_EQUIPMENT_CHANGED = priest_OnEquipmentChanged
+ Events.OnScalingEncode = priest_OnScalingEncode
+
+ priest_ScanTalents()
+ priest_ScanEquipment()
+ priest_UpdatePlayerScaling()
+end
+
+---------------------
+-- Effects: Shaman --
+---------------------
+
+-- Public Scaling: { [AstralShift] }
+-- We default to 0.3, because quite frankly nobody will use less points except while leveling
+local shaman_defaultScaling = {0.3}
+
+local function shaman_AstralShift_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local sourceScaling, quality = UnitScaling(srcGUID, shaman_defaultScaling, 0.7)
+ return -1, quality, sourceScaling[1]
+end
+
+local function shaman_AstralShift_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ local total = absorbedRemaining + overkill
+
+ return floor(total * effectEntry[7]), true
+end
+
+local function shaman_OnTalentUpdate()
+ -- Astral Shift
+ local t = select(5, GetTalentInfo(1, 21))
+ playerScaling[1] = t * 0.1
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.SHAMAN()
+ Events.PLAYER_TALENT_UPDATE = shaman_OnTalentUpdate
+ shaman_OnTalentUpdate()
+end
+
+----------------------
+-- Effects: Warlock --
+----------------------
+
+-- TODO: base leveling increase
+local warlock_Sacrifice_Spells = {
+ [7812] = 305,
+ [19438] = 510,
+ [19440] = 770,
+ [19441] = 1095,
+ [19442] = 1470,
+ [19443] = 1905,
+ [27273] = 2855,
+ [47985] = 6750,
+ [47986] = 8365
+}
+
+local warlock_ShadowWard_Spells = {
+ [6229] = 290,
+ [11739] = 470,
+ [11740] = 675,
+ [28610] = 875,
+ [47890] = 2750,
+ [47891] = 3300
+}
+
+-- Public Scaling: { [DemonicBrutality] }
+local warlock_defaultScaling = {1.0}
+
+-- No downranking support here
+local function warlock_Sacrifice_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ -- Note that the source is the voidwalker, so dest is the warlock!
+ local sourceScaling, quality = UnitScaling(dstGUID, warlock_defaultScaling, 0.4)
+ return floor(warlock_Sacrifice_Spells[spellid] * sourceScaling[1]), quality
+end
+
+local function warlock_ShadowWard_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_SHADOW then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function warlock_OnTalentUpdate()
+ -- Demonic Brutality
+ local t = select(5, GetTalentInfo(2, 6))
+ playerScaling[1] = 1 + (t * 0.1)
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.WARLOCK()
+ Events.PLAYER_TALENT_UPDATE = warlock_OnTalentUpdate
+ warlock_OnTalentUpdate()
+end
+
+--------------------
+-- Effects: Items --
+--------------------
+
+local function items_EssenceOfGossamer_Hit(effectEntry)
+ if effectEntry[3] < 140 then
+ return effectEntry[3], false
+ end
+ return 140, true
+end
+
+local function items_ArgussianCompass_Hit(effectEntry)
+ if effectEntry[3] < 68 then
+ return effectEntry[3], false
+ end
+ return 68, true
+end
+
+local function items_Valanyr_OnHeal(srcGUID, srcName, dstGUID, dstName, spellid, amount)
+ PushCharge(dstGUID, 64413, floor(amount * 0.15), 5.0)
+end
+
+local function items_Valanyr_OnAuraApplied(srcGUID, srcName, dstGUID, dstName, spellid)
+ Core.AddCombatTrigger(srcGUID, "OnHeal", items_Valanyr_OnHeal)
+end
+
+local function items_Valanyr_OnAuraRemoved(srcGUID, srcName, dstGUID, dstName, spellid)
+ Core.RemoveCombatTrigger(srcGUID, "OnHeal", items_Valanyr_OnHeal)
+end
+
+local function items_Valanyr_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local existing = 0
+
+ if destEffects and destEffects[spellid] then
+ existing = destEffects[spellid][3]
+ end
+
+ local charge = PopCharge(dstGUID, spellid)
+ if charge == 0 then
+ return existing, 0.0
+ end
+
+ -- According to the blue post explaining the Val'anyr effect on introduction, all units are
+ -- contributing to the same bubble with a cap of 20.000
+ return min(20000, existing + charge), 1.0
+end
+
+local function items_Stoicism_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local maxHealth = UnitDispatch(UnitHealthMax, dstGUID, dstName)
+ if maxHealth == 0 then
+ return 0, 0.0
+ end
+ return floor(maxHealth * 0.2), 1.0
+end
+
+-----------------
+-- Data Tables --
+-----------------
+
+local mage_FireWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, mage_FireWard_Hit, mage_Absorb_Spells, 0.8053}
+local mage_FrostWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, mage_FrostWard_Hit, mage_Absorb_Spells, 0.8053}
+local mage_IceBarrier_Entry = {1.0, 60, mage_IceBarrier_Create, generic_Hit}
+local mage_ManaShield_Entry = {1.0, 60, generic_SpellScalingByTable_Create, generic_Hit, mage_Absorb_Spells, 0.8053}
+local priest_PWS_Entry = {1.0, 30, priest_PowerWordShield_Create, generic_Hit}
+local warlock_Sacrifice_Entry = {1.0, 30, generic_ConstantByTable_Create, generic_Hit, warlock_Sacrifice_Spells}
+local warlock_ShadowWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, warlock_ShadowWard_Hit, warlock_ShadowWard_Spells, 0.8053}
+
+-- INCOMPLETE
+Core.Effects = {
+ -- Unknown Effect
+ -- This is used when a known effect is applied, but it is impossible to properly account for it,
+ -- for example if an AREA effect is applied with an unknown trigger
+ [0] = {1.0, 0, function() return 0, 0.0 end, nil},
+ [48707] = {3.0, 5, deathknight_AntiMagicShell_Create, deathknight_AntiMagicShell_Hit}, -- Anti-Magic Shell
+ [50461] = {-3.0, 10, deathknight_AntiMagicZone_Create, deathknight_AntiMagicZone_Hit}, -- Anti-Magic Zone
+
+ [52284] = {1.0, nil, function() return 0, 0.0 end, deathknight_WoN_Hit1}, -- Will of the Necropolis (Rank 1)
+ [52285] = {1.0, nil, function() return 0, 0.0 end, deathknight_WoN_Hit2}, -- Will of the Necropolis (Rank 2)
+ [52286] = {1.0, nil, function() return 0, 0.0 end, deathknight_WoN_Hit3}, -- Will of the Necropolis (Rank 3)
+
+ [62606] = {1.1, 10, druid_SavageDefense_Create, druid_SavageDefense_Hit}, -- Savage Defense
+ [543] = mage_FireWard_Entry, -- Fire Ward (rank 1)
+ [8457] = mage_FireWard_Entry, -- Fire Ward (rank 2)
+ [8458] = mage_FireWard_Entry, -- Fire Ward (rank 3)
+ [10223] = mage_FireWard_Entry, -- Fire Ward (rank 4)
+ [10225] = mage_FireWard_Entry, -- Fire Ward (rank 5)
+ [27218] = mage_FireWard_Entry, -- Fire Ward (rank 6)
+ [43010] = mage_FireWard_Entry, -- Fire Ward (rank 7)
+ [6143] = mage_FrostWard_Entry, -- Frost Ward (rank 1)
+ [8461] = mage_FrostWard_Entry, -- Frost Ward (rank 2)
+ [8462] = mage_FrostWard_Entry, -- Frost Ward (rank 3)
+ [10177] = mage_FrostWard_Entry, -- Frost Ward (rank 4)
+ [28609] = mage_FrostWard_Entry, -- Frost Ward (rank 5)
+ [32796] = mage_FrostWard_Entry, -- Frost Ward (rank 6)
+ [43012] = mage_FrostWard_Entry, -- Frost Ward (rank 7)
+ [11426] = mage_IceBarrier_Entry, -- Ice Barrier (rank 1)
+ [13031] = mage_IceBarrier_Entry, -- Ice Barrier (rank 2)
+ [13032] = mage_IceBarrier_Entry, -- Ice Barrier (rank 3)
+ [13033] = mage_IceBarrier_Entry, -- Ice Barrier (rank 4)
+ [27134] = mage_IceBarrier_Entry, -- Ice Barrier (rank 5)
+ [33405] = mage_IceBarrier_Entry, -- Ice Barrier (rank 6)
+ [43038] = mage_IceBarrier_Entry, -- Ice Barrier (rank 7)
+ [43039] = mage_IceBarrier_Entry, -- Ice Barrier (rank 8)
+ [1463] = mage_ManaShield_Entry, -- Mana shield (rank 1)
+ [8494] = mage_ManaShield_Entry, -- Mana shield (rank 2)
+ [8495] = mage_ManaShield_Entry, -- Mana shield (rank 3)
+ [10191] = mage_ManaShield_Entry, -- Mana shield (rank 4)
+ [10192] = mage_ManaShield_Entry, -- Mana shield (rank 5)
+ [10193] = mage_ManaShield_Entry, -- Mana shield (rank 6)
+ [27131] = mage_ManaShield_Entry, -- Mana shield (rank 7)
+ [43019] = mage_ManaShield_Entry, -- Mana shield (rank 8)
+ [43020] = mage_ManaShield_Entry, -- Mana shield (rank 9)
+ [58597] = {1.0, 6, paladin_SacredShield_Create, generic_Hit}, -- Sacred Shield
+ [17] = priest_PWS_Entry, -- Power Word: Shield (rank 1)
+ [592] = priest_PWS_Entry, -- Power Word: Shield (rank 2)
+ [600] = priest_PWS_Entry, -- Power Word: Shield (rank 3)
+ [3747] = priest_PWS_Entry, -- Power Word: Shield (rank 4)
+ [6065] = priest_PWS_Entry, -- Power Word: Shield (rank 5)
+ [6066] = priest_PWS_Entry, -- Power Word: Shield (rank 6)
+ [10898] = priest_PWS_Entry, -- Power Word: Shield (rank 7)
+ [10899] = priest_PWS_Entry, -- Power Word: Shield (rank 8)
+ [10900] = priest_PWS_Entry, -- Power Word: Shield (rank 9)
+ [10901] = priest_PWS_Entry, -- Power Word: Shield (rank 10)
+ [25217] = priest_PWS_Entry, -- Power Word: Shield (rank 11)
+ [25218] = priest_PWS_Entry, -- Power Word: Shield (rank 12)
+ [48065] = priest_PWS_Entry, -- Power Word: Shield (rank 13)
+ [48066] = priest_PWS_Entry, -- Power Word: Shield (rank 14)
+ [47753] = {1.0, 12, priest_DivineAegis_Create, generic_Hit}, -- Divine Aegis
+ [52179] = {2.5, nil, shaman_AstralShift_Create, shaman_AstralShift_Hit}, -- Astral Shift
+ [7812] = warlock_Sacrifice_Entry, -- Sacrifice (rank 1)
+ [19438] = warlock_Sacrifice_Entry, -- Sacrifice (rank 2)
+ [19440] = warlock_Sacrifice_Entry, -- Sacrifice (rank 3)
+ [19441] = warlock_Sacrifice_Entry, -- Sacrifice (rank 4)
+ [19442] = warlock_Sacrifice_Entry, -- Sacrifice (rank 5)
+ [19443] = warlock_Sacrifice_Entry, -- Sacrifice (rank 6)
+ [27273] = warlock_Sacrifice_Entry, -- Sacrifice (rank 7)
+ [47985] = warlock_Sacrifice_Entry, -- Sacrifice (rank 8)
+ [47986] = warlock_Sacrifice_Entry, -- Sacrifice (rank 9)
+ [6229] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 1)
+ [11739] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 1)
+ [11740] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 2)
+ [28610] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 3)
+ [47890] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 4)
+ [47891] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 5)
+ [64413] = {1.0, 8, items_Valanyr_Create, generic_Hit}, -- Val'anyr (spellid of the created absorb effect)
+ [60218] = {5.0, 10, function() return 4000, 1.0 end, items_EssenceOfGossamer_Hit}, -- Essence of Gossamer
+ [71586] = {1.0, 10, function() return 6400, 1.0 end, generic_Hit}, -- Corroded Skeleton Key
+ [36481] = {1.0, 4, function() return 100000, 1.0 end, generic_Hit}, -- Phaseshift Bulwark
+ [57350] = {1.0, 6, function() return 1500, 1.0 end, generic_Hit}, -- Darkmoon Card: Illusion
+ [17252] = {1.0, 1800, function() return 500, 1.0 end, generic_Hit}, -- Mark of the Dragon Lord
+ [29506] = {1.0, 20, function() return 900, 1.0 end, generic_Hit}, -- The Burrower's Shell
+ [31771] = {1.0, 20, function() return 440, 1.0 end, generic_Hit}, -- Runed Fungalcap
+ [9800] = {1.0, 60, function() return 175, 1.0 end, generic_Hit}, -- Truesilver Champion
+ [13234] = {1.0, 600, function() return 500, 1.0 end, generic_Hit}, -- Gnomish Harm Prevention Belt
+ [30458] = {1.0, 8, function() return 4000, 1.0 end, generic_Hit}, -- Nigh Invulnerability Belt
+ [27779] = {1.0, 30, function() return 350, 1.0 end, generic_Hit}, -- Divine Protection (Priest Dungeon Set 1/2 4pc bonus)
+ [28810] = {1.0, 30, function() return 500, 1.0 end, generic_Hit}, -- Armor of Faith (Priest Raid Set 3 4pc bonus)
+ [29674] = {1.0, nil, function() return 1000, 1.0 end, generic_Hit}, -- Lesser Ward of Shielding
+ [29719] = {1.0, nil, function() return 4000, 1.0 end, generic_Hit}, -- Greater Ward of Shielding
+ [29701] = {1.0, nil, function() return 4000, 1.0 end, generic_Hit}, -- Greater Ward of Shielding
+ [28538] = {1.0, 120, function() return 3400, 1.0 end, Holy_Hit}, -- Major Holy Protection Potion
+ [28537] = {1.0, 120, function() return 3400, 1.0 end, Shadow_Hit}, -- Major Shadow Protection Potion
+ [28536] = {1.0, 120, function() return 3400, 1.0 end, Arcane_Hit}, -- Major Arcane Protection Potion
+ [28513] = {1.0, 120, function() return 3400, 1.0 end, Nature_Hit}, -- Major Nature Protection Potion
+ [28512] = {1.0, 120, function() return 3400, 1.0 end, Frost_Hit}, -- Major Frost Protection Potion
+ [28511] = {1.0, 120, function() return 3400, 1.0 end, Fire_Hit}, -- Major Fire Protection Potion
+ [7233] = {1.0, 120, function() return 1300, 1.0 end, Fire_Hit}, -- Fire Protection Potion
+ [7239] = {1.0, 120, function() return 1300, 1.0 end, Frost_Hit}, -- Frost Protection Potion
+ [7242] = {1.0, 120, function() return 1300, 1.0 end, Shadow_Hit}, -- Shadow Protection Potion
+ [7245] = {1.0, 120, function() return 1300, 1.0 end, Holy_Hit}, -- Holy Protection Potion
+ [7254] = {1.0, 120, function() return 1300, 1.0 end, Nature_Hit}, -- Nature Protection Potion
+ [53915] = {1.0, 120, function() return 3100, 1.0 end, Shadow_Hit}, -- Mighty Shadow Protection Potion
+ [53914] = {1.0, 120, function() return 3100, 1.0 end, Nature_Hit}, -- Mighty Nature Protection Potion
+ [53913] = {1.0, 120, function() return 3100, 1.0 end, Frost_Hit}, -- Mighty Frost Protection Potion
+ [53911] = {1.0, 120, function() return 3100, 1.0 end, Fire_Hit}, -- Mighty Fire Protection Potion
+ [53910] = {1.0, 120, function() return 3100, 1.0 end, Arcane_Hit}, -- Mighty Arcane Protection Potion
+ [17548] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Shadow Protection Potion
+ [17546] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Nature Protection Potion
+ [17545] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Holy Protection Potion
+ [17544] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Frost Protection Potion
+ [17543] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Fire Protection Potion
+ [17549] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Arcane Protection Potion
+ [28527] = {1.0, 15, function() return 1000, 1.0 end, generic_Hit}, -- Fel Blossom
+ [29432] = {1.0, 3600, function() return 2000, 1.0 end, Fire_Hit}, -- Frozen Rune
+ [25750] = {1.0, 15, function() return 151, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [25747] = {1.0, 15, function() return 344, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [25746] = {1.0, 15, function() return 394, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [23991] = {1.0, 15, function() return 550, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [30997] = {1.0, 300, function() return 1800, 1.0 end, Fire_Hit}, -- Pendant of Frozen Flame Usage
+ [31002] = {1.0, 300, function() return 1800, 1.0 end, Arcane_Hit}, -- Pendant of the Null Rune
+ [30999] = {1.0, 300, function() return 1800, 1.0 end, Nature_Hit}, -- Pendant of Withering
+ [30994] = {1.0, 300, function() return 1800, 1.0 end, Frost_Hit}, -- Pendant of Thawing
+ [31000] = {1.0, 300, function() return 1800, 1.0 end, Shadow_Hit}, -- Pendant of Shadow's End
+ [23506] = {1.0, 20, function() return 1000, 1.0 end, generic_Hit}, -- Arena Grand Master
+ [12561] = {1.0, 60, function() return 400, 1.0 end, Fire_Hit}, -- Goblin Construction Helmet
+ [21956] = {1.0, 15, function() return 250, 1.0 end, Physical_Hit}, -- Mark of Resolution
+ [4057] = {1.0, 60, function() return 250, 1.0 end, Fire_Hit}, -- Flame Deflector
+ [4077] = {1.0, 60, function() return 300, 1.0 end, generic_Hit}, -- Ice Deflector
+ [39228] = {1.0, 20, function() return 609, 1.0 end, generic_Hit}, -- Argussian Compass (may not be an actual absorb)
+ [11657] = {1.0, 20, function() return 70, 1.0 end, generic_Hit}, -- Jang'thraze (Zul Farrak)
+ [10368] = {1.0, 15, function() return 1000, 1.0 end, generic_Hit}, -- Uther's Strength
+ [37515] = {1.0, 15, function() return 1000, 1.0 end, generic_Hit}, -- Warbringer Armor Proc
+ [42137] = {1.0, 86400, function() return 1000, 1.0 end, generic_Hit}, -- Greater Rune of Warding Proc
+ [26467] = {1.0, 30, function() return 1000, 1.0 end, generic_Hit}, -- Scarab Brooch
+ [26470] = {1.0, 8, function() return 1000, 1.0 end, generic_Hit}, -- Scarab Brooch
+ [27539] = {1.0, 6, function() return 1000, 1.0 end, generic_Hit}, -- Thick Obsidian Breatplate
+ [54808] = {1.0, 12, function() return 1000, 1.0 end, generic_Hit}, -- Noise Machine Sonic Shield
+ [55019] = {1.0, 12, function() return 1000, 1.0 end, generic_Hit}, -- Sonic Shield
+ [70845] = {1.0, 10, items_Stoicism_Create, generic_Hit}, -- Stoicism (Warrior Raid Set 10 4pc bonus)
+ [65686] = {1.0, 0, function() return 0, 0.0 end, nil}, -- Twin Val'kyr: Light Essence
+ [65684] = {1.0, 0, function() return 0, 0.0 end, nil} -- Twin Val'kyr: Dark Essence
+}
+
+Core.AreaTriggers = {
+ [51052] = 50461, -- Anti-Magic Zone
+ [62618] = 81781 -- Power Word: Barrier
+}
+
+Core.CombatTriggers = {
+ OnAuraApplied = {
+ [64411] = items_Valanyr_OnAuraApplied
+ },
+ OnAuraRemoved = {
+ [64411] = items_Valanyr_OnAuraRemoved
+ },
+ OnHeal = {},
+ OnHealCrit = {}
+}
+
+----------------
+-- Initialize --
+----------------
+
+if not Core.Available then
+ Core.RegisterEvent("PLAYER_ENTERING_WORLD")
+else
+ Core.Enable()
+end
+
+------------------------
+-- Callback Reference --
+------------------------
+
+-- EffectApplied
+-- (srcGUID, srcName, dstGUID, dstName, spellid, value, quality, duration)
+-- The effect-individual messages get sent on visible and non-visible effects
+
+-- EffectUpdated
+-- (guid, spellid, value, [duration, only if refreshed])
+
+-- EffectRemoved
+-- (guid, spellid)
+
+-- Whenever the unit that radiates an AREA effect is created (visible/non-visible)
+-- Note that the actual effect on the unit that absorbs damage casues an
+-- EffectApplied/EffectRemoved message, but not EffectUpdated (instead AreaUpdated)
+-- The rationale behind this is performance, since we cannot update every unit afflicted
+-- by the area effect on every hit. Therefore, we have the shared entry in the activeEffects
+-- table of each unit, and will handle it the same way when exporting - separately from each
+-- others.
+
+-- AreaCreated
+-- (srcGUID, srcName, triggerGUID, spellid, value, quality)
+
+-- AreaUpdated
+-- (triggerGUID, value)
+
+-- AreaCleared
+-- (triggerGUID)
+
+-- UnitUpdated
+-- (guid, value, quality)
+-- Only for VISIBLE changes on the total amount
+
+-- UnitCleared
+-- (guid)
+-- Everytime a unit gets cleared from all absorb effects (quality reset)
+-- including non-visible effects
\ No newline at end of file
diff --git a/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml
new file mode 100644
index 0000000..9d1a9f0
--- /dev/null
+++ b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file