diff --git a/Gladius/Changelog-Gladius-v1.2.0.txt b/Gladius/Changelog-Gladius-v1.2.1.txt similarity index 98% rename from Gladius/Changelog-Gladius-v1.2.0.txt rename to Gladius/Changelog-Gladius-v1.2.1.txt index f3fe85e..177295a 100644 --- a/Gladius/Changelog-Gladius-v1.2.0.txt +++ b/Gladius/Changelog-Gladius-v1.2.1.txt @@ -1,9 +1,9 @@ -tag v1.2.0 +tag v1.2.1 c728d6df2e4c16959912975a3ed09400323f5229 root 2010-07-14 14:36:07 +0000 -Tagging as v1.2.0 +Tagging as v1.2.1 -------------------- diff --git a/Gladius/Gladius.lua b/Gladius/Gladius.lua index acc4bca..5d1c939 100644 --- a/Gladius/Gladius.lua +++ b/Gladius/Gladius.lua @@ -1,6 +1,9 @@ Gladius = LibStub("AceAddon-3.0"):NewAddon("Gladius", "AceEvent-3.0", "AceConsole-3.0") local L = LibStub("AceLocale-3.0"):GetLocale("Gladius", true) local LSM = LibStub("LibSharedMedia-3.0") +local LCG = LibStub("LibCustomGlow-1.0") +local LAM = LibStub:GetLibrary("AbsorbsMonitor-1.0", true) + local arenaUnits = {} local arenaGUID = {} local arenaSpecs = {} @@ -144,13 +147,18 @@ function Gladius:ClearAllUnits() v.lastAuraName = nil -- diminishing return - v.diminishingReturn = {} + v.diminishingReturn = {} -- reset cooldown tracker for i=1, 14 do - local icon = v.spellCooldownFrame["icon" .. i] - icon.spellId = nil - icon:Hide() + local icon = v.spellCooldownFrame["icon" .. i] + icon.spellId = nil + --reset Glow settings + if( icon.glowActive ) then + if( db.cooldownAuraGlow ) then LCG.ButtonGlow_Stop(icon) end + icon.glowActive = false; + end + icon:Hide() end -- Reset all the cooldown spirals @@ -158,6 +166,7 @@ function Gladius:ClearAllUnits() -- Turn grid trinket icon green again v.gridTrinket:SetBackdropColor(0,1,0,1) + v.bigGridTrinket:SetBackdropColor(0,1,0,1) -- Reset the trinket "text" to avoid issues with the embedded trinket icon v.trinket:SetText("") @@ -183,6 +192,7 @@ function Gladius:JoinedArena() -- Enemy events self:RegisterEvent("UNIT_AURA") self:RegisterEvent("UNIT_HEALTH") + --self:RegisterEvent("UNIT_MAXHEALTH") self:RegisterEvent("UNIT_MANA", "UNIT_POWER") self:RegisterEvent("UNIT_RAGE", "UNIT_POWER") self:RegisterEvent("UNIT_ENERGY", "UNIT_POWER") @@ -266,11 +276,16 @@ function Gladius:UNIT_HEALTH(event, unit) local button = self.buttons[unit] if(not button) then return end + -- update absorb bar + if( db.absorbBar ) then + Gladius:UpdateAbsorb(event, unit, button) + end + -- show the button if (arenaUnits[unit] == "playerUnit" or (arenaUnits[unit] ~= "playerUnit" and db.showPets)) then - if (not button:IsShown()) then button:Show() end - if (button:GetAlpha() < 1) then button:SetAlpha(1) end - end + if (not button:IsShown()) then button:Show() end + if (button:GetAlpha() < 1) then button:SetAlpha(1) end + end if(not UnitIsDeadOrGhost(unit)) then local currentHealth, maxHealth = UnitHealth(unit), UnitHealthMax(unit) @@ -383,15 +398,21 @@ end local DRINK_SPELL = GetSpellInfo(57073) local WYVERN_SPELL = GetSpellInfo(49012) function Gladius:UNIT_AURA(event, unit) + if ( arenaUnits[unit] == "playerUnit" ) then local button = self.buttons[unit] if(not button) then return end + -- update absorb bar + if( db.absorbBar) then + Gladius:UpdateAbsorb(event, unit, button) + end + -- show the button if (arenaUnits[unit] == "playerUnit" or (arenaUnits[unit] ~= "playerUnit" and db.showPets)) then - if (not button:IsShown()) then button:Show() end - if (button:GetAlpha() < 1) then button:SetAlpha(1) end - end + if (not button:IsShown()) then button:Show() end + if (button:GetAlpha() < 1) then button:SetAlpha(1) end + end local aura = button.auraFrame local index = 1 @@ -410,10 +431,10 @@ function Gladius:UNIT_AURA(event, unit) auraExpTime = expirationTime -- announce aura gain - if((db.auraAnnounce or (db.auraAnnounceList[auraName] and db.auraAnnounceList[auraName] ~= "disabled")) and (not button.lastAuraName or auraName ~= button.lastAuraName)) then - self:SendAnnouncement(string.format(L["AURA GAIN: %s (%s) - %s for %s seconds"], UnitName(unit), UnitClass(unit), auraName, ceil(auraExpTime - GetTime())), RAID_CLASS_COLORS[select(2, UnitClass(unit))], db.auraAnnounceList[auraName] and db.auraAnnounceList[auraName] or db.announceType) - button.lastAuraName = auraName - end + if((db.auraAnnounce or (db.auraAnnounceList[auraName] and db.auraAnnounceList[auraName] ~= "disabled")) and (not button.lastAuraName or auraName ~= button.lastAuraName)) then + self:SendAnnouncement(string.format(L["AURA GAIN: %s (%s) - %s for %s seconds"], UnitName(unit), UnitClass(unit), auraName, ceil(auraExpTime - GetTime())), RAID_CLASS_COLORS[select(2, UnitClass(unit))], db.auraAnnounceList[auraName] and db.auraAnnounceList[auraName] or db.announceType) + button.lastAuraName = auraName + end end -- Spec detection @@ -465,6 +486,7 @@ function Gladius:UNIT_AURA(event, unit) end end end + end end @@ -679,6 +701,7 @@ function Gladius:DetectSpec(unit, spec) local icon = button.spellCooldownFrame["icon" .. button.lastCooldownSpell] icon:Show() icon.texture:SetTexture(self.spellTextures[k]) + icon.spellId = k -- Missing spellID to updated talent frame button.lastCooldownSpell = button.lastCooldownSpell + 1 end @@ -875,34 +898,88 @@ function Gladius:DRPositionIcons(unit) end end +function Gladius:StartCooldownGlow(unit, spellId, auraType) + local button = self.buttons[unit] + if not button then return end + if (db.cooldownList[spellId] == false and auraType == 'DEBUFF') then return end + + for i=1,(button.lastCooldownSpell or 14) do -- button.lastCooldownSpell return a number that match max number of detected talent cooldown + if (button.spellCooldownFrame["icon" .. i] == nil) then return end + if (button.spellCooldownFrame["icon" .. i].spellId == spellId) then + local frame = button.spellCooldownFrame["icon" .. i] + frame.glowActive = true + -- reset desaturated / opacity style while its active + frame.cooldown:Hide() + if( db.cooldownDesaturate ) then frame.texture:SetDesaturated(0) end + if( db.cooldownOpacity ) then frame:SetAlpha(1) end + -- set Glow + if( db.cooldownAuraGlow ) then + LCG.ButtonGlow_Start(frame) + end + end + end + +end + +function Gladius:StopCooldownGlow(unit, spellId, auraType) + local button = self.buttons[unit] + if not button then return end + if (db.cooldownList[spellId] == false and auraType == 'BUFF') then return end + + for i=1,(button.lastCooldownSpell or 14) do + if (button.spellCooldownFrame["icon" .. i].spellId == spellId) then + local frame = button.spellCooldownFrame["icon" .. i] + frame.glowActive = false + -- desaturated / opacity style if true + frame.cooldown:Show() + if( db.cooldownDesaturate ) then frame.texture:SetDesaturated(1) end + if( db.cooldownOpacity ) then frame:SetAlpha(db.cooldownOpacityValue) end + -- set Glow + if( db.cooldownAuraGlow ) then + LCG.ButtonGlow_Stop(frame) + end + end + end + +end + --Scan combatlog for enemy deaths and aura fading -function Gladius:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...) +function Gladius:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...) -- spellID, spellName, _, type -- enemy death if (eventType == "PARTY_KILL" and bit.band(destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) == COMBATLOG_OBJECT_REACTION_HOSTILE) then self:UnitDeath(destGUID) elseif ( eventType == "SPELL_AURA_APPLIED" ) then - local spellID, spellName = ... + local spellID, spellName, _, type = ... + -- SetGlow + if (arenaGUID[destGUID] ~= nil) then + self:StartCooldownGlow(arenaGUID[sourceGUID], spellID, type) + end + -- Aura check if ( arenaGUID[destGUID] and ( spellID == 49010 or spellID == 49009 or spellID == 27069 or spellID == 24135 or spellID == 24134 or spellID == 24131 ) ) then self.buttons[arenaGUID[destGUID]].wyvernDot = true - elseif (arenaGUID[destGUID] and self.drSpellIds[spellName]) then - self:DRGain(arenaGUID[destGUID], self.drSpellIds[spellName]) + elseif (arenaGUID[destGUID] and self.drSpellIds[spellName]) then + self:DRGain(arenaGUID[destGUID], self.drSpellIds[spellName]) end elseif (eventType == "SPELL_AURA_REMOVED" or eventType == "SPELL_PERIODIC_AURA_REMOVED" or eventType == "SPELL_AURA_REMOVED_DOSE" or eventType == "SPELL_PERIODIC_AURA_REMOVED_DOSE") then - local spellID, spellName = ... + local spellID, spellName, _, type = ... + -- StopGlow + if (arenaGUID[destGUID] ~= nil) then + self:StopCooldownGlow(arenaGUID[sourceGUID], spellID, type) + end if ( arenaGUID[destGUID] and ( spellID == 49010 or spellID == 49009 or spellID == 27069 or spellID == 24135 or spellID == 24134 or spellID == 24131 ) ) then self.buttons[arenaGUID[destGUID]].wyvernDot = false - elseif (arenaGUID[destGUID] and self.drSpellIds[spellName]) then - self:DRFades(arenaGUID[destGUID], spellName) + elseif (arenaGUID[destGUID] and self.drSpellIds[spellName]) then + self:DRFades(arenaGUID[destGUID], spellName) end - elseif (eventType == "SPELL_AURA_REFRESH") then - local spellID, spellName = ... - if (arenaGUID[destGUID] and self.drSpellIds[spellName]) then - self:DRFades(arenaGUID[destGUID], spellName) - self:DRGain(arenaGUID[destGUID], self.drSpellIds[spellName]) - end + elseif (eventType == "SPELL_AURA_REFRESH") then + local spellID, spellName = ... + if (arenaGUID[destGUID] and self.drSpellIds[spellName]) then + self:DRFades(arenaGUID[destGUID], spellName) + self:DRGain(arenaGUID[destGUID], self.drSpellIds[spellName]) + end elseif ( eventType == "SPELL_CAST_SUCCESS" ) then local spellID, spellName = ... @@ -956,6 +1033,7 @@ function Gladius:UNIT_SPELLCAST_SUCCEEDED(event, unit, spell, rank) end function Gladius:CooldownUsed(unit, unitClass, spellId, spellName) + local button = self.buttons[unit] if not button then return end if (db.cooldownList[spellId] == false) then return end @@ -1017,15 +1095,12 @@ function Gladius:CooldownStart(button, spellId, duration) frame.active = true frame.timeLeft = duration frame.cooldown:SetCooldown(GetTime(), duration) - if( db.cooldownDesaturate ) then - frame.texture:SetDesaturated(1) - end - if( db.cooldownOpacity ) then - frame:SetAlpha(db.cooldownOpacityValue) - end - - frame:SetScript("OnUpdate", function(self, elapsed) - self.timeLeft = self.timeLeft - elapsed + -- + if( db.cooldownDesaturate ) then frame.texture:SetDesaturated(1) end + if( db.cooldownOpacity ) then frame:SetAlpha(db.cooldownOpacityValue) end + -- + frame:SetScript("OnUpdate", function(self, elapsed) -- problem here, it seems like 2 cooldown used close one to each other get additionated + self.timeLeft = self.timeLeft - elapsed if ( self.timeLeft <= 0 ) then Gladius:CooldownReady(button, spellId, frame) end @@ -1047,24 +1122,22 @@ function Gladius:CooldownReady(button, spellId, frame) if( db.cooldownOpacity ) then frame:SetAlpha(1) end - frame.cooldown:Hide() + --frame.cooldown:Hide() -- no point in hidding frame:SetScript("OnUpdate", nil) end end - else - - frame.active = false - if( db.cooldownDesaturate ) then - frame.texture:SetDesaturated(0) - end - if( db.cooldownOpacity ) then - frame:SetAlpha(1) - end - frame.cooldown:Hide() - frame:SetScript("OnUpdate", nil) - - end + else + frame.active = false + if( db.cooldownDesaturate ) then + frame.texture:SetDesaturated(0) + end + if( db.cooldownOpacity ) then + frame:SetAlpha(1) + end + --frame.cooldown:Hide() -- no point in hidding + frame:SetScript("OnUpdate", nil) + end end function Gladius:GetTrinketIcon(unit) @@ -1608,3 +1681,56 @@ function Gladius:Test() end end + +-- Absorb Update +function Gladius:UpdateAbsorb(event, unit, button) + + local health = UnitHealth(unit) + local maxHealth = UnitHealthMax(unit) + local _guid = UnitGUID(unit) + local myCurrentHealAbsorb = LAM.Unit_Total(_guid) + + --LAM.SetLowValueTolerance(0) + --print("unit: "..unit.." | "..myCurrentHealAbsorb) + + -- + function CompactUnitFrameUtil_UpdateFillBar(self, frame, previousTexture, health, myCurrentHealAbsorb) + local totalWidth, totalHeight = frame:GetSize(); + local prevWidth, prevHeight = previousTexture:GetSize(); + local amout = (health + myCurrentHealAbsorb) / maxHealth + local barOffsetX = (health / maxHealth) * totalWidth + local barOffsetXPercent = previousTexture:GetWidth() * amout + + local barSize = barOffsetXPercent - barOffsetX + if barSize + barOffsetX > totalWidth then + barSize = totalWidth - barOffsetX + end + + self:ClearAllPoints() + self:SetPoint("LEFT", previousTexture:GetStatusBarTexture(), "RIGHT") + self:SetSize(barSize, previousTexture:GetHeight()) + self:Show() + end + -- + + if ( myCurrentHealAbsorb > 0 and health < maxHealth ) then + CompactUnitFrameUtil_UpdateFillBar(button.absorb.totalAbsorbOverlay, button, button.health, health, myCurrentHealAbsorb) + CompactUnitFrameUtil_UpdateFillBar(button.absorb.totalAbsorb, button, button.health, health, myCurrentHealAbsorb) + else + button.absorb.totalAbsorbOverlay:Hide() + button.absorb.totalAbsorb:Hide() + end + + local overAbsorb = false; + if ( health - myCurrentHealAbsorb > maxHealth or health + myCurrentHealAbsorb > maxHealth ) then + overAbsorb = true; + myCurrentHealAbsorb = max(0, maxHealth - health); + end + + if ( overAbsorb ) then + button.absorb.overAbsorbGlow:Show(); + else + button.absorb.overAbsorbGlow:Hide(); + end + +end \ No newline at end of file diff --git a/Gladius/Gladius.toc b/Gladius/Gladius.toc index d27b368..9c64090 100644 --- a/Gladius/Gladius.toc +++ b/Gladius/Gladius.toc @@ -1,10 +1,10 @@ ## Interface: 30300 -## Title: |cff1784d1Gladius|r |cffe5e3e3Enhanced|r +## Title: |cffe5e3e3Gladius|r |cff1784d1Enhanced|r ## Notes: Arena enemy unit frames -## Version: v1.2.1 -## Author: Proditor, Rinu +## Version: v1.2.2 +## Author: Proditor, Rinu, Enhanced by Cortes ## SavedVariables: GladiusDB -## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets +## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0 ## LoadManagers: AddonLoader ## X-LoadOn-Slash: /gladius ## X-LoadOn-InterfaceOptions: Gladius diff --git a/Gladius/cooldownlist.lua b/Gladius/cooldownlist.lua index 0b7eb58..bc47844 100644 --- a/Gladius/cooldownlist.lua +++ b/Gladius/cooldownlist.lua @@ -156,7 +156,6 @@ function Gladius:GetCooldownList() [781] = 15, -- Disengage [5384] = 20, -- Feign Death [3045] = 20, -- Rapid Fire - [60192] = { cd = 28, -- Freezing Arrow sharedCD = { [14311] = true, -- Freezing Trap @@ -176,7 +175,6 @@ function Gladius:GetCooldownList() }, }, [34600] = { cd = 28, }, -- Snake Trap - [34490] = { cd = 20, spec = L["Marksmanship"], }, -- Silencing Shot [19386] = { cd = 60, spec = L["Survival"], }, -- Wyvern Sting [53271] = { cd = 60, pet = true, }, -- Masters Call diff --git a/Gladius/embeds.xml b/Gladius/embeds.xml index 2fa969d..73d45cb 100644 --- a/Gladius/embeds.xml +++ b/Gladius/embeds.xml @@ -12,5 +12,7 @@ + + \ No newline at end of file diff --git a/Gladius/frame.lua b/Gladius/frame.lua index af12627..71b916b 100644 --- a/Gladius/frame.lua +++ b/Gladius/frame.lua @@ -186,25 +186,25 @@ function Gladius:CreateButton(i) button:Hide() --selected frame (solid frame around the players target) - button.selected = CreateFrame("Frame", nil, button) - button.selected:SetBackdrop({edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", edgeSize = 1,}) - button.selected:SetFrameStrata("HIGH") - button.selected:Hide() + button.selected = CreateFrame("Frame", nil, button) + button.selected:SetBackdrop({edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", edgeSize = 1,}) + button.selected:SetFrameStrata("HIGH") + button.selected:Hide() - --focus frame (solid frame around the players focus) - local focusBorder = CreateFrame("Frame", nil, button) - focusBorder:SetBackdrop({edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", edgeSize = 1,}) - focusBorder:SetFrameStrata("LOW") - focusBorder:Hide() + --focus frame (solid frame around the players focus) + local focusBorder = CreateFrame("Frame", nil, button) + focusBorder:SetBackdrop({edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", edgeSize = 1,}) + focusBorder:SetFrameStrata("LOW") + focusBorder:Hide() - -- assist frame (solid frame around the raid main assist target) - local assistBorder = CreateFrame("Frame", nil, button) - assistBorder:SetBackdrop({edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", edgeSize = 1,}) - assistBorder:SetFrameStrata("MEDIUM") - assistBorder:Hide() + -- assist frame (solid frame around the raid main assist target) + local assistBorder = CreateFrame("Frame", nil, button) + assistBorder:SetBackdrop({edgeFile = "Interface\\ChatFrame\\ChatFrameBackground", edgeSize = 1,}) + assistBorder:SetFrameStrata("MEDIUM") + assistBorder:Hide() --Health bar - local healthBar = CreateFrame("StatusBar", nil, button) + local healthBar = CreateFrame("StatusBar", "GladiusHealthBar"..i, button) healthBar:ClearAllPoints() healthBar:SetPoint("TOPLEFT",button,"TOPLEFT", classIconSize, 0) healthBar:SetPoint("BOTTOMRIGHT",button,"BOTTOMRIGHT") @@ -224,6 +224,24 @@ function Gladius:CreateButton(i) healthBar.highlight:SetAllPoints(healthBar) healthBar.highlight:Hide() + -- AbsorbBar + local absorbBar = CreateFrame("Frame", "GladiusAbsorbBar"..i, button) + local overAbsorbFrame = CreateFrame("Frame", "GladiusOverAbsorbGlow"..i, healthBar) -- hack to get the glow spark over everything + -- OverAbsorb + absorbBar.overAbsorbGlow = overAbsorbFrame:CreateTexture(nil, "OVERLAY") + absorbBar.overAbsorbGlow:SetTexture([[Interface\AddOns\Gladius\media\RaidFrame\Shield-Overshield]]) + absorbBar.overAbsorbGlow:SetBlendMode("ADD"); + absorbBar.overAbsorbGlow:Hide() + -- Total absorb + absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BORDER", nil, 0) + absorbBar.totalAbsorb:SetTexture([[Interface\AddOns\Gladius\media\RaidFrame\Shield-Fill]]) + absorbBar.totalAbsorb:Hide() + -- Total absorb overlay + absorbBar.totalAbsorbOverlay = absorbBar:CreateTexture(nil, "BORDER", nil, 1) + absorbBar.totalAbsorbOverlay:SetHorizTile(true) + absorbBar.totalAbsorbOverlay:SetTexture([[Interface\AddOns\Gladius\media\RaidFrame\Shield-Overlay]], "MIRROR") + absorbBar.totalAbsorbOverlay:Hide() + --Mana bar local manaBar = CreateFrame("StatusBar", nil, button) manaBar:ClearAllPoints() @@ -430,6 +448,7 @@ function Gladius:CreateButton(i) button.mana = manaBar button.health = healthBar + button.absorb = absorbBar button.castBar = castBar button.castBar.timeText = castBarTextTime button.castBar.spellText = castBarTextSpell @@ -760,6 +779,20 @@ function Gladius:UpdateFrame() DisableTexTiling(button.health:GetStatusBarTexture()) DisableTexTiling(button.health.bg) + -- absorb bar location and size + -- OverAbsorb + button.absorb.overAbsorbGlow:ClearAllPoints() + button.absorb.overAbsorbGlow:SetPoint("RIGHT", button.health, "RIGHT", 7, 0) + button.absorb.overAbsorbGlow:SetSize(12, button.health:GetHeight() + 4) + -- Total absorb + button.absorb.totalAbsorb:ClearAllPoints() + button.absorb.totalAbsorb:SetPoint("RIGHT", button.health, "RIGHT") + button.absorb.totalAbsorb:SetSize(15, button.health:GetHeight()) + -- Total absorb overlay + button.absorb.totalAbsorbOverlay:ClearAllPoints() + button.absorb.totalAbsorbOverlay:SetPoint("RIGHT", button.health, "RIGHT") + button.absorb.totalAbsorbOverlay:SetSize(15, button.health:GetHeight()) -- -15 + --mana bar location, size and texture button.mana:ClearAllPoints() button.mana:SetHeight(db.manaBarHeight) @@ -780,64 +813,64 @@ function Gladius:UpdateFrame() button.castBar:ClearAllPoints() local castBarX = db.castBarHeight if (db.castBarPos == "RIGHT") then - castBarX = db.castBarHeight + 5 + castBarX = db.castBarHeight + 5 - if (db.targetIcon) then - castBarX = castBarX + targetIconSize - end + if (db.targetIcon) then + castBarX = castBarX + targetIconSize + end - if (db.trinketDisplay == "bigIcon" or db.trinketDisplay == "bigGridIcon" and db.trinketStatus) then - castBarX = castBarX + (classIconSize+1)*db.bigTrinketScale - end + if (db.trinketDisplay == "bigIcon" or db.trinketDisplay == "bigGridIcon" and db.trinketStatus) then + castBarX = castBarX + (classIconSize+1)*db.bigTrinketScale + end - if (db.enableDebuffs and db.debuffPos == "RIGHT") then - if(db.debuffs[3].name ~= "") then - castBarX = castBarX + db.barHeight + db.manaBarHeight + 5 - else - castBarX = castBarX + (db.barHeight + db.manaBarHeight) / 2 + 5 - end - end + if (db.enableDebuffs and db.debuffPos == "RIGHT") then + if(db.debuffs[3].name ~= "") then + castBarX = castBarX + db.barHeight + db.manaBarHeight + 5 + else + castBarX = castBarX + (db.barHeight + db.manaBarHeight) / 2 + 5 + end + end - if (db.cooldown and db.cooldownPos == "RIGHT") then - castBarX = castBarX + (db.barHeight + db.manaBarHeight + 5) * 3.5 - end - elseif (db.castBarPos == "LEFT") then - castBarX = -db.castBarWidth - 5 + if (db.cooldown and db.cooldownPos == "RIGHT") then + castBarX = castBarX + (db.barHeight + db.manaBarHeight + 5) * 3.5 + end + elseif (db.castBarPos == "LEFT") then + castBarX = -db.castBarWidth - 5 - if (db.enableDebuffs and db.debuffPos == "LEFT") then - if(db.debuffs[3].name ~= "") then - castBarX = castBarX - db.barHeight - db.manaBarHeight - 5 - else - castBarX = castBarX - (db.barHeight + db.manaBarHeight) / 2 - 5 - end - end + if (db.enableDebuffs and db.debuffPos == "LEFT") then + if(db.debuffs[3].name ~= "") then + castBarX = castBarX - db.barHeight - db.manaBarHeight - 5 + else + castBarX = castBarX - (db.barHeight + db.manaBarHeight) / 2 - 5 + end + end - if (db.cooldown and db.cooldownPos == "LEFT") then - castBarX = castBarX - (db.barHeight + db.manaBarHeight + 5) * 3.5 - end - else - if (db.classIcon and db.auraPos == "LEFT") then - castBarX = castBarX - classIconSize - end - end + if (db.cooldown and db.cooldownPos == "LEFT") then + castBarX = castBarX - (db.barHeight + db.manaBarHeight + 5) * 3.5 + end + else + if (db.classIcon and db.auraPos == "LEFT") then + castBarX = castBarX - classIconSize + end + end if (db.powerBar) then - local parent = db.castBarPos == "CENTER" and button.mana or button + local parent = db.castBarPos == "CENTER" and button.mana or button button.castBar:SetPoint("TOPLEFT",parent, db.castBarPos == "CENTER" and "BOTTOMLEFT" or "TOP" .. db.castBarPos,castBarX,0) else - local parent = db.castBarPos == "CENTER" and button.health or button + local parent = db.castBarPos == "CENTER" and button.health or button button.castBar:SetPoint("TOPLEFT",parent,db.castBarPos == "CENTER" and "BOTTOMLEFT" or "TOP" .. db.castBarPos,castBarX,0) end button.castBar:SetHeight(db.castBarHeight) if (db.castBarPos ~= "CENTER") then - button.castBar:SetWidth(db.castBarWidth) + button.castBar:SetWidth(db.castBarWidth) else - if (db.classIcon) then - button.castBar:SetWidth(button.health:GetWidth()+classIconSize+targetIconSize-db.castBarHeight) - else - button.castBar:SetWidth(button.health:GetWidth()+targetIconSize-db.castBarHeight) - end - end + if (db.classIcon) then + button.castBar:SetWidth(button.health:GetWidth()+classIconSize+targetIconSize-db.castBarHeight) + else + button.castBar:SetWidth(button.health:GetWidth()+targetIconSize-db.castBarHeight) + end + end button.castBar:SetStatusBarTexture(LSM:Fetch(LSM.MediaType.STATUSBAR, db.barTexture)) button.castBar:SetStatusBarColor(db.castBarColor.r,db.castBarColor.g,db.castBarColor.b,db.castBarColor.a) button.castBar.bg:SetTexture(LSM:Fetch(LSM.MediaType.STATUSBAR, db.barTexture)) @@ -871,8 +904,8 @@ function Gladius:UpdateFrame() --healthbar color if(button.class and db.healthBarClassColor) then button.health:SetStatusBarColor(RAID_CLASS_COLORS[button.class].r, RAID_CLASS_COLORS[button.class].g, RAID_CLASS_COLORS[button.class].b, 1.0) - else - button.health:SetStatusBarColor(db.healthBarColor.r, db.healthBarColor.g, db.healthBarColor.b, 1.0) + else + button.health:SetStatusBarColor(db.healthBarColor.r, db.healthBarColor.g, db.healthBarColor.b, 1.0) end --power bar colors @@ -920,8 +953,8 @@ function Gladius:UpdateFrame() -- set secure targettarget button.secureTarget:ClearAllPoints() - button.secureTarget:SetHeight(targetIconSize) - button.secureTarget:SetWidth(targetIconSize) + button.secureTarget:SetHeight(targetIconSize) + button.secureTarget:SetWidth(targetIconSize) button.secureTarget:SetPoint("TOPRIGHT",button,"TOPRIGHT", targetIconX, 0) button.secureTarget:SetFrameStrata("HIGH") @@ -958,9 +991,10 @@ function Gladius:UpdateFrame() button.bigTrinket:SetTexture(Gladius:GetTrinketIcon("player", false)) button.bigTrinket:ClearAllPoints() local parent = db.targetIcon and button.targetIcon or button - local bigTrinketX = (classIconSize+1)*db.bigTrinketScale + local bigTrinketX = (classIconSize+1)*db.bigTrinketScale + db.bigTrinketOffsetX + local bigTrinketY = db.bigTrinketOffsetY if (db.classIcon and db.auraPos == "RIGHT" and db.targetIcon) then bigTrinketX = 2*(classIconSize+1)*db.bigTrinketScale end - button.bigTrinket:SetPoint("TOPRIGHT", parent ,"TOPRIGHT", bigTrinketX, 0) + button.bigTrinket:SetPoint("TOPRIGHT", parent ,"TOPRIGHT", bigTrinketX, bigTrinketY) if (db.trinketDisplay ~= "bigIcon") then button.bigTrinket:Hide() @@ -971,12 +1005,12 @@ function Gladius:UpdateFrame() --the big GRID trinket icon button.bigGridTrinket:SetWidth(classIconSize*db.bigTrinketScale) button.bigGridTrinket:SetHeight((classIconSize+1)*db.bigTrinketScale) - button.bigGridTrinket:SetBackdropColor(0,1,0,1) button.bigGridTrinket:ClearAllPoints() local parentGrid = db.targetIcon and button.targetIcon or button - local bigGridTrinketX = (classIconSize+1)*db.bigTrinketScale + local bigGridTrinketX = (classIconSize+1)*db.bigTrinketScale + db.bigTrinketOffsetX + local bigGridTrinketY = db.bigTrinketOffsetY if (db.classIcon and db.auraPos == "RIGHT" and db.targetIcon) then bigGridTrinketX = 2*(classIconSize+1)*db.bigTrinketScale end - button.bigGridTrinket:SetPoint("TOPRIGHT", parentGrid ,"TOPRIGHT", bigGridTrinketX, 0) + button.bigGridTrinket:SetPoint("TOPRIGHT", parentGrid ,"TOPRIGHT", bigGridTrinketX, bigGridTrinketY) if (db.trinketDisplay ~= "bigGridIcon") then button.bigGridTrinket:Hide() @@ -1117,54 +1151,54 @@ function Gladius:UpdateFrame() -- Cooldown frame if (db.cooldown) then - button.spellCooldownFrame:ClearAllPoints() - if(db.cooldownPos == "RIGHT") then - if(db.trinketStatus and db.trinketDisplay == "bigIcon" or db.trinketDisplay == "bigGridIcon") then - local spellCooldownX = 5 - if (db.enableDebuffs and db.debuffPos == "RIGHT") then - if (db.debuffs[3].name ~= "") then - spellCooldownX = db.barHeight + db.manaBarHeight + 10 - elseif (db.debuffs[1].name ~= "") then - spellCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 - end - end - if( db.trinketDisplay == "bigIcon" ) then button.spellCooldownFrame:SetPoint("TOPLEFT",button.bigTrinket,"TOPRIGHT",spellCooldownX,-1) end - if( db.trinketDisplay == "bigGridIcon" ) then button.spellCooldownFrame:SetPoint("TOPLEFT",button.bigGridTrinket,"TOPRIGHT",spellCooldownX,-1) end + button.spellCooldownFrame:ClearAllPoints() + if(db.cooldownPos == "RIGHT") then + if(db.trinketStatus and db.trinketDisplay == "bigIcon" or db.trinketDisplay == "bigGridIcon") then + local spellCooldownX = 5 + if (db.enableDebuffs and db.debuffPos == "RIGHT") then + if (db.debuffs[3].name ~= "") then + spellCooldownX = db.barHeight + db.manaBarHeight + 10 + elseif (db.debuffs[1].name ~= "") then + spellCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 + end + end + if( db.trinketDisplay == "bigIcon" ) then button.spellCooldownFrame:SetPoint("TOPLEFT",button.bigTrinket,"TOPRIGHT",spellCooldownX,-1) end + if( db.trinketDisplay == "bigGridIcon" ) then button.spellCooldownFrame:SetPoint("TOPLEFT",button.bigGridTrinket,"TOPRIGHT",spellCooldownX,-1) end + else + local relFrame = db.targetIcon and button.targetIcon or button + local spellCooldownX = 5 + if (db.enableDebuffs and db.debuffPos == "RIGHT") then + if (db.debuffs[3].name ~= "") then + spellCooldownX = db.barHeight + db.manaBarHeight + 10 + elseif (db.debuffs[1].name ~= "") then + spellCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 + end + end + button.spellCooldownFrame:SetPoint("TOPLEFT",relFrame,"TOPRIGHT",spellCooldownX,-1) + end else - local relFrame = db.targetIcon and button.targetIcon or button + local relFrame = button local spellCooldownX = 5 - if (db.enableDebuffs and db.debuffPos == "RIGHT") then - if (db.debuffs[3].name ~= "") then - spellCooldownX = db.barHeight + db.manaBarHeight + 10 - elseif (db.debuffs[1].name ~= "") then - spellCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 - end - end - button.spellCooldownFrame:SetPoint("TOPLEFT",relFrame,"TOPRIGHT",spellCooldownX,-1) + if (db.enableDebuffs and db.debuffPos == "LEFT") then + if (db.debuffs[3].name ~= "") then + spellCooldownX = db.barHeight + db.manaBarHeight + 10 + elseif (db.debuffs[1].name ~= "") then + spellCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 + end end - else - local relFrame = button - local spellCooldownX = 5 - if (db.enableDebuffs and db.debuffPos == "LEFT") then - if (db.debuffs[3].name ~= "") then - spellCooldownX = db.barHeight + db.manaBarHeight + 10 - elseif (db.debuffs[1].name ~= "") then - spellCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 - end - end button.spellCooldownFrame:SetPoint("TOPRIGHT",relFrame,"TOPLEFT",-spellCooldownX,-1) end - button.spellCooldownFrame:SetHeight(db.barHeight+extraBarHeight) - button.spellCooldownFrame:SetWidth(db.barHeight+extraBarHeight) - - -- Update each cooldown icon - for i=1,14 do - local icon = button.spellCooldownFrame["icon"..i] - icon:SetHeight(button.spellCooldownFrame:GetHeight()/2) - icon:SetWidth(button.spellCooldownFrame:GetWidth()/2) - icon:ClearAllPoints() + button.spellCooldownFrame:SetHeight(db.barHeight+extraBarHeight) + button.spellCooldownFrame:SetWidth(db.barHeight+extraBarHeight) + + -- Update each cooldown icon + for i=1,14 do + local icon = button.spellCooldownFrame["icon"..i] + icon:SetHeight(button.spellCooldownFrame:GetHeight()/2) + icon:SetWidth(button.spellCooldownFrame:GetWidth()/2) + icon:ClearAllPoints() - if(db.cooldownPos == "RIGHT") then + if(db.cooldownPos == "RIGHT") then if(i==1) then icon:SetPoint("TOPLEFT",button.spellCooldownFrame) elseif(i==2) then @@ -1183,71 +1217,71 @@ function Gladius:UpdateFrame() end if (icon.active) then - icon.active = false - icon.cooldown:SetCooldown(GetTime(), 0) - icon:SetScript("OnUpdate", nil) - end + icon.active = false + icon.cooldown:SetCooldown(GetTime(), 0) + icon:SetScript("OnUpdate", nil) + end - icon.spellId = nil - icon:SetAlpha(1) - icon.texture:SetTexture("Interface\\Icons\\Spell_Holy_PainSupression") - StyleActionButton(icon) + icon.spellId = nil + icon:SetAlpha(1) + icon.texture:SetTexture("Interface\\Icons\\Spell_Holy_PainSupression") + StyleActionButton(icon) - if (not self.frame.testing) then - icon:Hide() - else - icon:Show() - end - end - button.spellCooldownFrame:Show() - else - button.spellCooldownFrame:Hide() - end + if (not self.frame.testing) then + icon:Hide() + else + icon:Show() + end + end + button.spellCooldownFrame:Show() + else + button.spellCooldownFrame:Hide() + end - -- diminishing return frame + -- diminishing return frame if (db.drCooldown) then - -- frame position - local anchor = db.drCooldownAnchor == "CENTER" and "" or db.drCooldownAnchor - - button.drCooldownFrame:ClearAllPoints() - if(db.drCooldownPos == "RIGHT") then - if(db.trinketStatus and db.trinketDisplay == "bigIcon" or db.trinketDisplay == "bigGridIcon") then - local drCooldownX = 5 - if (db.enableDebuffs and db.debuffPos == "RIGHT") then - if (db.debuffs[3].name ~= "") then - drCooldownX = db.barHeight + db.manaBarHeight + 10 - elseif (db.debuffs[1].name ~= "") then - drCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 - end - end - if( db.trinketDisplay == "bigIcon" ) then button.drCooldownFrame:SetPoint("TOPLEFT",button.bigTrinket,"TOPRIGHT",drCooldownX+db.drMargin-5,-1) end - if( db.trinketDisplay == "bigGridIcon" ) then button.drCooldownFrame:SetPoint("TOPLEFT",button.bigGridTrinket,"TOPRIGHT",drCooldownX+db.drMargin-5,-1) end + -- frame position + local anchor = db.drCooldownAnchor == "CENTER" and "" or db.drCooldownAnchor + + button.drCooldownFrame:ClearAllPoints() + if(db.drCooldownPos == "RIGHT") then + if(db.trinketStatus and db.trinketDisplay == "bigIcon" or db.trinketDisplay == "bigGridIcon") then + local drCooldownX = 5 + if (db.enableDebuffs and db.debuffPos == "RIGHT") then + if (db.debuffs[3].name ~= "") then + drCooldownX = db.barHeight + db.manaBarHeight + 10 + elseif (db.debuffs[1].name ~= "") then + drCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 + end + end + if( db.trinketDisplay == "bigIcon" ) then button.drCooldownFrame:SetPoint("TOPLEFT",button.bigTrinket,"TOPRIGHT",drCooldownX+db.drMargin-5,-1) end + if( db.trinketDisplay == "bigGridIcon" ) then button.drCooldownFrame:SetPoint("TOPLEFT",button.bigGridTrinket,"TOPRIGHT",drCooldownX+db.drMargin-5,-1) end else - local relFrame = db.targetIcon and button.targetIcon or button - local drCooldownX = 5 - if (db.enableDebuffs and db.debuffPos == "RIGHT") then - if (db.debuffs[3].name ~= "") then - drCooldownX = db.barHeight + db.manaBarHeight + 10 - elseif (db.debuffs[1].name ~= "") then - drCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 - end - end + local relFrame = db.targetIcon and button.targetIcon or button + local drCooldownX = 5 + if (db.enableDebuffs and db.debuffPos == "RIGHT") then + if (db.debuffs[3].name ~= "") then + drCooldownX = db.barHeight + db.manaBarHeight + 10 + elseif (db.debuffs[1].name ~= "") then + drCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 + end + end button.drCooldownFrame:SetPoint("TOPLEFT",relFrame,"TOPRIGHT",drCooldownX+db.drMargin-5,-1) end else - local relFrame = button - local drCooldownX = 5 - if (db.enableDebuffs and db.debuffPos == "LEFT") then - if (db.debuffs[3].name ~= "") then - drCooldownX = db.barHeight + db.manaBarHeight + 10 - elseif (db.debuffs[1].name ~= "") then - drCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 - end - end + local relFrame = button + local drCooldownX = 5 + if (db.enableDebuffs and db.debuffPos == "LEFT") then + if (db.debuffs[3].name ~= "") then + drCooldownX = db.barHeight + db.manaBarHeight + 10 + elseif (db.debuffs[1].name ~= "") then + drCooldownX = (db.barHeight + db.manaBarHeight) / 2 + 10 + end + end button.drCooldownFrame:SetPoint("TOPRIGHT",relFrame,"TOPLEFT",-drCooldownX-db.drMargin+5,-1) end - button.drCooldownFrame:SetHeight(db.barHeight+extraBarHeight) - button.drCooldownFrame:SetWidth(db.barHeight+extraBarHeight) + button.drCooldownFrame:SetHeight(db.barHeight+extraBarHeight) + button.drCooldownFrame:SetWidth(db.barHeight+extraBarHeight) -- Update each cooldown icon for i=1,16 do @@ -1314,9 +1348,9 @@ function Gladius:UpdateFrame() end end button.drCooldownFrame:Show() - else - button.drCooldownFrame:Hide() - end + else + button.drCooldownFrame:Hide() + end --do some extra updates if the frame is in test mode if (self.frame.testing) then @@ -1363,6 +1397,17 @@ function Gladius:UpdateFrame() button.healthText:SetText(healthText) + --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 + --set the mana text local currentMana, maxMana = button.manaActual, button.manaMax local manaPercent = button.manaPercentage diff --git a/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua b/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua new file mode 100644 index 0000000..aa9ffd3 --- /dev/null +++ b/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.lua @@ -0,0 +1,2323 @@ + +------------------------------------------------------------------------ +-- +-- AbsorbsMonitor +-- +-- Copyright (C) 2010 Philipp Schmidt +-- +-- 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: +-- +-- Free Software Foundation, Inc., +-- 51 Franklin Street, Fifth Floor, +-- Boston, MA 02110-1301, USA. +-- +-- +------------------------------------------------------------------------ + + + +local AM_Public, upgraded = LibStub:NewLibrary("AbsorbsMonitor-1.0", 1); + +if(not AM_Public) then return; end + +local AM_Core; + + + +--------------------- +-- Install/Upgrade -- +--------------------- + +-- 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. + -- The only thing we want to keep is the callback registry + 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 + ); + + 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); +end + + + +--------------------- +-- Local Variables -- +--------------------- + +local AM_Callbacks = AM_Core.Callbacks; +local AM_Events = AM_Core.Events; + +local playerGUID; +local 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; + +-- 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 AM_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 AM_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 AM_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 AM_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 AM_Core.EffectInfo +local Effects; + +-- Table of spells that cause an area effect +-- [spellId (trigger)] = spellId (absorb effect) +-- Shortcut to AM_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 AM_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 AM_Core.UnitStats +local UnitStats; + +-- 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 AM_Core.Scaling and its entries +local Scaling; +local privateScaling; +local playerScaling; + +-- 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.3; -- "constant" + + + +---------------------- +-- Helper functions -- +---------------------- + +local CommStatsCooldown = false; +local function ClearCommStatsCooldown() + CommStatsCooldown = false; +end + +local CommScalingCooldown = false; +local function ClearCommScalingCooldown() + CommScalingCooldown = false; +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 + +local function SortEffects(a, b) + if(a[2] == b[2]) then + return (a[3] < b[3]); + else + return (a[2] > b[2]); + end +end + +-- Tries to get a working unitId +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(UnitGUID("target") == guid) then + result = func("target"); + elseif(UnitGUID("focus") == guid) then + result = func("focus"); + end + end + + return result; +end + + + +-------------------- +-- Core functions -- +-------------------- + +function AM_Core.Enable() + _, playerClass = UnitClass("player"); + 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; + + 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] = {}} + 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); + + 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() + 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 + +function AM_Core.RegisterEvent(name) + AM_Core.Frame:RegisterEvent(name); +end + +function AM_Core.UnregisterEvent(name) + AM_Core.Frame:UnregisterEvent(name); +end + +function AM_Core.Print(str) + DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99AbsMon|r: "..str) +end + +function AM_Core.ApplySingularEffect(sourceGUID, sourceName, destGUID, destName, spellId) + 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 + 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 + 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 + + 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 + -- reporting range anyway + effectEntry[6] = AM_Core:ScheduleTimer(AM_Events.OnSingularTimeout, effectInfo[2] + 5, {destGUID, spellId}); + 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) + if(not activeAreaEffects[triggerGUID]) then + return ApplySingularEffect(triggerGUID, triggerName, destGUID, destName, 0); + end + + 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 + 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 ~= destGUID) then + tinsert(activeEffectsByPriority[destGUID], effectEntry); + 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 + +function AM_Core.CreateAreaTrigger(sourceGUID, sourceName, triggerGUID, triggerName, spellId) + if(activeAreaEffects[triggerGUID]) then + 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, 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); + + 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 + +local AM_Fire = AM_Callbacks.Fire; +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 + effectEntry[3] = effectEntry[3] - absorbed; + absorbedRemaining = absorbedRemaining - absorbed; + + 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 + -- 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 + + -- 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 + AM_Fire(AM_Core, "UnitUpdated", guid, guidEffects[-1], guidEffects[-2]); + end +end + +-- Note that this method should NOT be called on non-existing effects or units +-- There are no exist checks within it. +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 + + else + if(effectEntry[2] < 2) then + 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 + 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 + 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 AM_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 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; + + -- 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 AM_Core.AddCombatTrigger(target, event, func) + local eventTriggers = AM_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 + for k, v in pairs(funcList) do + if(v == func) then + 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}; + eventTriggers[listIndex] = funcList; + + local handler = function(...) + for k, v in pairs(funcList) do + v(...); + end + end + + eventTriggers[target] = handler; + 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 + 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 + + -- It was a direct call anyway + else + eventTriggers[target] = nil; + 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 + +function AM_Core.SendScaling() + 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 +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 AM_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 AM_Core.SetActive() + 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 + +ApplySingularEffect = AM_Core.ApplySingularEffect; +ApplyAreaEffect = AM_Core.ApplyAreaEffect; +CreateAreaTrigger = AM_Core.CreateAreaTrigger; +HitUnit = AM_Core.HitUnit; +RemoveActiveEffect = AM_Core.RemoveActiveEffect; + + + +--------------------- +-- Event functions -- +--------------------- + +function AM_Events.PLAYER_ENTERING_WORLD() + if(not GetTalentInfo(1, 1)) then + 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 + +function AM_Events.COMBAT_LOG_EVENT_UNFILTERED(timestamp, type, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...) + lastCombatLogEvent = timestamp; + + 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); + + elseif(type == "SWING_MISSED") then + local missType, amountMissed = select(1, ...); + + 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, ...); + + if(CombatTriggersOnHeal[sourceGUID]) then + CombatTriggersOnHeal[sourceGUID](sourceGUID, sourceName, destGUID, destName, spellId, amount, overhealing); + end + + 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); + else + 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); + else + 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, ...); + + 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 + curChatChannel = "BATTLEGROUND"; + elseif(UnitInRaid("player")) then + curChatChannel = "RAID"; + elseif(GetNumPartyMembers() > 0) then + curChatChannel = "PARTY"; + else + curChatChannel = nil; + end +end + +-- Due to be removed in 4.0 +function AM_Events.ZONE_CHANGED_NEW_AREA() + if(UnitInBattleground("player")) then + ZONE_MODIFIER = 1.17; + elseif(IsActiveBattlefieldArena()) then + ZONE_MODIFIER = 0.9; + elseif(GetRealZoneText() == "Wintergrasp") then + -- 1.3 * 0.9 = fail Blizzard + ZONE_MODIFIER = 1.17; + elseif(GetRealZoneText() == "Icecrown Citadel") then + ZONE_MODIFIER = 1.3; + else + ZONE_MODIFIER = 1.3; + end +end + +function AM_Events.STATS_CHANGED() + local baseAP, plusAP, minusAP = UnitAttackPower("player"); + + UnitStats[playerGUID][2] = baseAP + plusAP - minusAP; + -- TODO: What about spell power ~= healing spell power? + UnitStats[playerGUID][3] = GetSpellBonusHealing(); + + if(curChatChannel) then + AM_Core:ScheduleUniqueTimer("comm_stats", AM_Core.SendUnitStats, CommStatsCooldown and 15 or 5); + end +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 + UnitStats[guid][2] = ap; + UnitStats[guid][3] = sp; + UnitStats[guid][4] = 1.0; + end +end + +function AM_Events.OnScalingEncode() + return Scaling[playerGUID]; +end + +function OnScalingDecode.DEFAULT(guid, guidScaling) + Scaling[guid] = guidScaling; +end + +function AM_Events.OnScalingReceived(prefix, text, distribution, target) + if(not text) then return; end + + 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 + +function AM_Events.OnPeriodicBroadcast() + if(not curChatChannel) then return; end + + local playerStats = UnitStats[playerGUID]; + local playerScaling = Scaling[playerGUID]; + + 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 +end + +function AM_Events.OnSingularTimeout(args) + local guid, spellId = args[1], args[2]; + + if(activeEffectsBySpell[guid] and activeEffectsBySpell[guid][spellId]) then + RemoveActiveEffect(guid, spellId); + end +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 + -- Make sure to remove the timer + AM_Core:CancelTimer(activeEffectsBySpell[guid][spellId][6]); + end +end + +-- This should be called in even less cases than the normal timeout +function AM_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 + error("Positive refcount "..areaEntry[9].." remained for area effect "..areaEntry[1].." by trigger "..areaEntry[8]); +end + +-- Map client events to our callbacks +AM_Events.PARTY_MEMBERS_CHANGED = AM_Events.GROUPING_CHANGED; +AM_Events.RAID_ROSTER_UPDATE = AM_Events.GROUPING_CHANGED; +AM_Events.PLAYER_DAMAGE_DONE_MODS = AM_Events.STATS_CHANGED; +AM_Events.UNIT_ATTACK_POWER = AM_Events.STATS_CHANGED; + + + +---------------------- +-- Public functions -- +---------------------- + +function AM_Public.RegisterEffectCallbacks(self, funcApplied, funcUpdated, funcRemoved) + AM_Public.RegisterCallback(self, "EffectApplied", funcApplied); + AM_Public.RegisterCallback(self, "EffectUpdated", funcUpdated or funcApplied); + AM_Public.RegisterCallback(self, "EffectRemoved", funcRemoved or funcApplied); +end + +function AM_Public.RegisterUnitCallbacks(self, funcUpdated, funcCleared) + AM_Public.RegisterCallback(self, "UnitUpdated", funcUpdated); + AM_Public.RegisterCallback(self, "UnitCleared", funcCleared or funcUpdated); +end + +function AM_Public.RegisterAreaCallbacks(self, funcCreated, funcUpdated, funcCleared) + AM_Public.RegisterCallback(self, "AreaCreated", funcCreated); + AM_Public.RegisterCallback(self, "AreaUpdated", funcUpdated or funcCreated); + AM_Public.RegisterCallback(self, "AreaCleared", funcCleared or funcCreated); +end + +function AM_Public.GetLowValueTolerance() + return LOW_VALUE_TOLERANE; +end + +function AM_Public.SetLowValueTolerance(value) + LOW_VALUE_TOLERANCE = tonumber(value); +end + +function AM_Public.PrintProfiling() + if(GetCVar("scriptProfile") ~= "1") then + AM_Core.Print("CPU profiling disabled"); + return; + end + + 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, + ["RemoveActiveEffect"] = RemoveActiveEffect, + ["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 +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 + +function AM_Public.Unit_Scaling(guid, defaultScaling, defaultQuality) + local guidScaling = Scaling[guid]; + + if(guidScaling) then + return guidScaling, 1.0; + else + return defaultScaling, defaultQuality; + end +end + +-- Optimized method to save one function call on creation, since a lot of spells +-- actually require stats and scaling +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; + else + return guidStats[2], guidStats[3], guidStats[4], defaultScaling, defaultQuality; + end + else + if(guidScaling) then + return 0, 0, missingQuality, guidScaling, 1.0; + else + return 0, 0, missingQuality, defaultScaling, defaultQuality; + end + end +end + +function AM_Public.Unit_EffectsMap(guid) + return activeEffectsBySpell[guid]; +end + +function AM_Public.Unit_EffectsList(guid) + return activeEffectsByPriority[guid]; +end + +function AM_Public.ScheduleScalingBroadcast() + if(curChatChannel) then + AM_Core:ScheduleUniqueTimer("comm_scaling", AM_Core.SendScaling, CommScalingCooldown and 30 or 5); + end +end + +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); + ApplySingularEffect("0x0", "source", "0x1b", "dest B", 57350); + ApplySingularEffect("0x0", "source", "0x1c", "dest C", 48066); + ApplySingularEffect("0x0", "source", "0x1c", "dest C", 28810); +end + + + +------------------------------ +-- Generic Effect functions -- +------------------------------ + +local PushCharge = AM_Core.PushCharge; +local PopCharge = AM_Core.PopCharge; +local Unit_Stats = AM_Public.Unit_Stats; +local Unit_Scaling = AM_Public.Unit_Scaling; +local Unit_StatsAndScaling = AM_Public.Unit_StatsAndScaling; + +--- Generic Create function (only for documentary purposes) +-- @param sourceGUID guid of the originating unit +-- @param sourceName name of the originating unit +-- @param destGUID guid of the affected unit +-- @param destName name of the affected unit +-- @param spellId the spellId of this absorb effect +-- @param destEffects activeEffectsBySpell[destGUID] +-- @return total value of this absorb effect +-- @return quality (that is accuracy) of this value +local function generic_Create(sourceGUID, sourceName, destGUID, destName, 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(sourceGUID, sourceName, destGUID, destName, 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(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + local effectInfo = Effects[spellId]; + local _, sp, quality = Unit_Stats(sourceGUID, 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 + + + +--------------------------- +-- 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}; + + +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) + -- 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; + else + return maxAbsorb, true; + end +end + +local function deathknight_AntiMagicZone_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + local ap, _, quality = Unit_Stats(sourceGUID, 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; + else + return maxAbsorb, true; + end +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 + + + +-------------------- +-- Effects: Druid -- +-------------------- + +local function druid_SavageDefense_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + local ap, _, quality = Unit_Stats(sourceGUID, 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; + else + return 0, true; + end +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(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 + +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 = GetGlyphSocketInfo(i); + + -- Glyph of Ice Barrier + if(glyphSpellId and glyphSpellId == 63095) then + playerScaling[1] = 1.3; + + break; + end + end + + AM_Public.ScheduleScalingBroadcast(); +end + +function OnEnableClass.MAGE() + AM_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(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + local _, sp, quality1, sourceScaling, quality2 = Unit_StatsAndScaling(sourceGUID, 0.1, paladin_defaultScaling, 0.2); + + return floor((500 + (sp * 0.75)) * sourceScaling[1] * ZONE_MODIFIER), math.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 = GetTalentInfo(2, 9); + + playerScaling[1] = 1 + (t * 0.1); + + AM_Public.ScheduleScalingBroadcast(); +end + +function OnEnableClass.PALADIN() + AM_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(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 + +local function priest_PowerWordBarrier_Create(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + return 0, 0.0; +end + +local function priest_PowerWordBarrier_Hit(effectEntry, absorbedRemaining, overkill, spellSchool) + return 0, false; +end + +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; + + 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(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 + guidScaling = {}; + Scaling[guid] = guidScaling; + else + guidScaling = Scaling[guid]; + end + + guidScaling[PRIEST_DIVINEAEGIS_SPELLID] = daFactor; + + if(daFactor > 0) then + AM_Core.AddCombatTrigger(guid, "OnHealCrit", priest_DivineAegis_OnHealCrit); + 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 + 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(playerGUID, UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA); + + 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); + privateScaling["DivineAegis"] = t; + + -- Borrowed Time + local _, _, _, _, t = GetTalentInfo(1, 27); + privateScaling["BorrowedTime"] = t; + + -- Spiritual Healing + local _, _, _, _, t = 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() + AM_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() + AM_Events.PLAYER_LEVEL_UP = priest_OnLevelUp; + AM_Events.PLAYER_TALENT_UPDATE = priest_OnTalentUpdate; + AM_Events.PLAYER_EQUIPMENT_CHANGED = priest_OnEquipmentChanged; + + AM_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(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + local sourceScaling, quality = Unit_Scaling(sourceGUID, 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 = 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 + + + +---------------------- +-- 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] = 8350, + -- There is a new rank of Sacrifice at 79, so it scales by 15 to 80 + [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(sourceGUID, sourceName, destGUID, destName, spellId, destEffects) + -- Note that the source is the voidwalker, so dest is the warlock! + local sourceScaling, quality = Unit_Scaling(destGUID, 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 = GetTalentInfo(2, 6); + + playerScaling[1] = 1 + (t * 0.1); + + AM_Public.ScheduleScalingBroadcast(); +end + +function OnEnableClass.WARLOCK() + AM_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; + else + return 140, true; + end +end + +local function items_ArgussianCompass_Hit(effectEntry) + if(effectEntry[3] < 68) then + return effectEntry[3], false; + else + return 68, true; + end +end + +local function items_Valanyr_OnAuraApplied(sourceGUID, sourceName, destGUID, destName, spellId) + AM_Core.AddCombatTrigger(sourceGUID, "OnHeal", items_Valanyr_OnHeal); +end + +local function items_Valanyr_OnAuraRemoved(sourceGUID, sourceName, destGUID, destName, spellId) + AM_Core.RemoveCombatTrigger(sourceGUID, "OnHeal", items_Valanyr_OnHeal); +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; +end + +local function items_Valanyr_OnHeal(sourceGUID, sourceName, destGUID, destName, spellId, amount) + PushCharge(destGUID, 64413, floor(amount * 0.15), 5.0); +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; +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 +-- This table gets a lot smaller in Cataclysm +AM_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}; + + -- 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, + [8457] = mage_FireWard_Entry, + [8458] = mage_FireWard_Entry, + [10223] = mage_FireWard_Entry, + [10225] = mage_FireWard_Entry, + [27218] = mage_FireWard_Entry, + [43010] = mage_FireWard_Entry, + -- Frost Ward + [6143] = mage_FrostWard_Entry, + [8461] = mage_FrostWard_Entry, + [8462] = mage_FrostWard_Entry, + [10177] = mage_FrostWard_Entry, + [28609] = mage_FrostWard_Entry, + [32796] = mage_FrostWard_Entry, + [43012] = mage_FrostWard_Entry, + -- Ice Barrier + [11426] = mage_IceBarrier_Entry, + [13031] = mage_IceBarrier_Entry, + [13032] = mage_IceBarrier_Entry, + [13033] = mage_IceBarrier_Entry, + [27134] = mage_IceBarrier_Entry, + [33405] = mage_IceBarrier_Entry, + [43038] = mage_IceBarrier_Entry, + [43039] = mage_IceBarrier_Entry, + -- Mana Shield + [1463] = mage_ManaShield_Entry, + [8494] = mage_ManaShield_Entry, + [8495] = mage_ManaShield_Entry, + [10191] = mage_ManaShield_Entry, + [10192] = mage_ManaShield_Entry, + [10193] = mage_ManaShield_Entry, + [27131] = mage_ManaShield_Entry, + [43019] = mage_ManaShield_Entry, + [43020] = mage_ManaShield_Entry, + + -- PALADIN + -- Sacred Shield + [58597] = {1.0, 6, paladin_SacredShield_Create, generic_Hit}, + -- Ardent Defender + + -- PRIEST + -- Power Word: Shield + [17] = priest_PWS_Entry, + [592] = priest_PWS_Entry, + [600] = priest_PWS_Entry, + [3747] = priest_PWS_Entry, + [6065] = priest_PWS_Entry, + [10898] = priest_PWS_Entry, + [10899] = priest_PWS_Entry, + [10900] = priest_PWS_Entry, + [10901] = priest_PWS_Entry, + [25217] = priest_PWS_Entry, + [25218] = priest_PWS_Entry, + [48065] = priest_PWS_Entry, + [48066] = priest_PWS_Entry, + -- Divine Aegis + [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}, + -- Glyph of Stoneclaw Totem + -- 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 + -- 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 + -- be constant for every rank. + -- The normal totem absorb effect could be tracked with more ease, but still + -- 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, + [19438] = warlock_Sacrifice_Entry, + [19440] = warlock_Sacrifice_Entry, + [19441] = warlock_Sacrifice_Entry, + [19442] = warlock_Sacrifice_Entry, + [19443] = warlock_Sacrifice_Entry, + [27273] = warlock_Sacrifice_Entry, + [47985] = warlock_Sacrifice_Entry, + [47986] = warlock_Sacrifice_Entry, + -- Shadow Ward + [6229] = warlock_ShadowWard_Entry, + [11739] = warlock_ShadowWard_Entry, + [11740] = warlock_ShadowWard_Entry, + [28610] = warlock_ShadowWard_Entry, + [47890] = warlock_ShadowWard_Entry, + [47891] = warlock_ShadowWard_Entry, + + -- ITEMS + -- Val'anyr (spellId of the created absorb effect) + [64413] = {1.0, 8, items_Valanyr_Create, generic_Hit}, + -- Essence of Gossamer + [60218] = {5.0, 10, function() return 4000, 1.0; end, items_EssenceOfGossamer_Hit}, + -- Corroded Skeleton Key + [71586] = {1.0, 10, function() return 6400, 1.0; end, generic_Hit}, + -- Phaseshift Bulwark + [36481] = {1.0, 4, function() return 100000, 1.0; end, generic_Hit}, + -- Darkmoon Card: Illusion + [57350] = {1.0, 6, function() return 1500, 1.0; end, generic_Hit}, + -- Mark of the Dragon Lord + [17252] = {1.0, 1800, function() return 500, 1.0; end, generic_Hit}, + -- The Burrower's Shell + [29506] = {1.0, 20, function() return 900, 1.0; end, generic_Hit}, + -- Arena Grand Master (mean value of 1000) + [23506] = {1.0, 20, function() return 1000, 0.5; end, generic_Hit}, + -- Argussian Compass + [39228] = {5.0, 20, function() return 1150, 1.0; end, items_ArgussianCompass_Hit}, + -- Runed Fungalcap + [31771] = {1.0, 20, function() return 440, 1.0; end, generic_Hit}, + -- Truesilver Champion + [9800] = {1.0, 60, function() return 175, 1.0; end, generic_Hit}, + -- Gnomish Harm Prevention Belt + [13234] = {1.0, 600, function() return 500, 1.0; end, generic_Hit}, + -- Nigh Invulnerability Belt + [30458] = {1.0, 8, function() return 4000, 1.0; end, generic_Hit}, + -- Divine Protection (Priest Dungeon Set 1/2 4pc bonus) + [27779] = {1.0, 30, function() return 350, 1.0; end, generic_Hit}, + -- Armor of Faith (Priest Raid Set 3 4pc bonus) + [28810] = {1.0, 30, function() return 500, 1.0; end, generic_Hit}, + -- Lesser Ward of Shielding + [29674] = {1.0, nil, function() return 1000, 1.0; end, generic_Hit}, + -- Greater Ward of Shielding + [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 + -- if a previously stronger shield gets hit and reduced below a might-be-created + -- shield coming from a following heal? It also seems like only a certain + -- set of healing spells attribute to bubbles +}; + + +AM_Core.AreaTriggers = { + -- Anti-Magic Zone + [51052] = 50461, + + -- Power Word: Barrier + [62618] = 81781, +}; + + +AM_Core.CombatTriggers = { + OnAuraApplied = { + [64411] = items_Valanyr_OnAuraApplied, + }, + + OnAuraRemoved = { + [64411] = items_Valanyr_OnAuraRemoved, + }, + + OnHeal = { + }, + + OnHealCrit = { + } +}; + + + +---------------- +-- Initialize -- +---------------- + +if(not AM_Core.Available) then + AM_Core.RegisterEvent("PLAYER_ENTERING_WORLD"); +else + AM_Core.Enable(); +end + + + +------------------------ +-- Callback Reference -- +------------------------ + +-- EffectApplied +-- (sourceGUID, sourceName, destGUID, destName, 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 +-- (sourceGUID, sourceName, 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 diff --git a/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.xml b/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.xml new file mode 100644 index 0000000..78209ae --- /dev/null +++ b/Gladius/libs/AbsorbsMonitor-1.0/AbsorbsMonitor-1.0.xml @@ -0,0 +1,4 @@ + +