542 lines
14 KiB
Lua
542 lines
14 KiB
Lua
--[[
|
|
API:
|
|
* RegisterCallback(addon, callback)
|
|
`callback` is called whenever some heal state (new heal/ heal stop/ heal delay) changes.
|
|
callback`'s arguments will be all units affected by the change in heal state, e.g.,
|
|
`callback("Tankguy", "Dpsguy")`.
|
|
|
|
* UnregisterCallback(addon)
|
|
Remove all callbacks registered by `addon`.
|
|
|
|
* UnitGetIncomingHeals(unit[, healer])
|
|
Return predicted incoming heals on unit. If `healer`, only predict incoming heals from healer.
|
|
]]
|
|
local ADDON_NAME = "HealPredict"
|
|
|
|
-- Wow API
|
|
local CheckInteractDistance = CheckInteractDistance
|
|
local CreateFrame = CreateFrame
|
|
local GetInventoryItemLink = GetInventoryItemLink
|
|
local GetLocale = GetLocale
|
|
local GetNumRaidMembers = GetNumRaidMembers
|
|
local GetSpellInfo = GetSpellInfo
|
|
local GetTime = GetTime
|
|
local SendAddonMessage = SendAddonMessage
|
|
local strjoin = strjoin
|
|
local strsplit = strsplit
|
|
local UIParent = UIParent
|
|
local UnitBuff = UnitBuff
|
|
local UnitCanAssist =UnitCanAssist
|
|
local UnitCastingInfo = UnitCastingInfo
|
|
local UnitChannelInfo = UnitChannelInfo
|
|
local UnitHealth = UnitHealth
|
|
local UnitHealthMax = UnitHealthMax
|
|
local UnitInRaid = UnitInRaid
|
|
local UnitName = UnitName
|
|
local UnitIsDeadOrGhost = UnitIsDeadOrGhost
|
|
|
|
-- Addon message constants:
|
|
local HEALSTOP = "HealStop"
|
|
local HEALDELAY = "HealDelay"
|
|
local HEAL = "Heal"
|
|
local SEP = "/"
|
|
|
|
-- Localize spell names:
|
|
local BEACON_OF_LIGHT
|
|
do
|
|
local locales = {
|
|
deDE = "Flamme des Glaubens",
|
|
enUS = "Beacon of Light",
|
|
esES = "Señal de la Luz",
|
|
esMX = "Señal de la Luz",
|
|
frFR = "Guide de lumière",
|
|
itIT = "Faro di Luce",
|
|
koKR = "빛의 봉화",
|
|
ptBR = "Foco de Luz",
|
|
ruRU = "Частица Света",
|
|
zhCN = "圣光道标",
|
|
zhTW = "聖光信標",
|
|
}
|
|
|
|
BEACON_OF_LIGHT = locales[GetLocale()] or locales.enUS
|
|
end
|
|
|
|
local CHAIN_HEAL
|
|
do
|
|
local locales = {
|
|
deDE = "Kettenheilung",
|
|
enUS = "Chain Heal",
|
|
esES = "Sanación en cadena",
|
|
esMX = "Sanación en cadena",
|
|
frFR = "Salve de guérison",
|
|
itIT = "Catena di Guarigione",
|
|
koKR = "연쇄 치유",
|
|
ptBR = "Cura Encadeada",
|
|
ruRU = "Цепное исцеление",
|
|
zhCN = "治疗链",
|
|
zhTW = "治療鍊",
|
|
}
|
|
|
|
CHAIN_HEAL = locales[GetLocale()] or locales.enUS
|
|
end
|
|
|
|
local PRAYER_OF_HEALING
|
|
do
|
|
local locales = {
|
|
deDE = "Gebet der Heilung",
|
|
enUS = "Prayer of Healing",
|
|
esES = "Rezo de curación",
|
|
esMX = "Rezo de sanación",
|
|
frFR = "Prière de soins",
|
|
itIT = "Preghiera di Cura",
|
|
koKR = "치유의 기원",
|
|
ptBR = "Prece de Cura",
|
|
ruRU = "Молитва исцеления",
|
|
zhCN = "治疗祷言",
|
|
zhTW = "治療禱言",
|
|
}
|
|
|
|
PRAYER_OF_HEALING = locales[GetLocale()] or locales.enUS
|
|
end
|
|
|
|
local PRAYER_OF_PRESERVATION = "Prayer of Preservation"
|
|
|
|
local TRANQUILITY
|
|
do
|
|
local locales = {
|
|
deDE = "Gelassenheit",
|
|
enUS = "Tranquility",
|
|
esES = "Tranquilidad",
|
|
esMX = "Tranquilidad",
|
|
frFR = "Tranquillité",
|
|
itIT = "Tranquillità",
|
|
koKR = "평온",
|
|
ptBR = "Tranquilidade",
|
|
ruRU = "Спокойствие",
|
|
zhCN = "宁静",
|
|
zhTW = "寧靜",
|
|
}
|
|
|
|
TRANQUILITY = locales[GetLocale()] or locales.enUS
|
|
end
|
|
|
|
local SMART_HEALS = { }
|
|
SMART_HEALS[TRANQUILITY] = 5
|
|
SMART_HEALS[PRAYER_OF_PRESERVATION] = 5
|
|
SMART_HEALS[CHAIN_HEAL] = 3
|
|
|
|
-- Addon locals
|
|
local player = UnitName("player")
|
|
local heals, callbacks, cache, gear_string = { }, { }, { }, ""
|
|
local is_healing, beacon_info, current_target
|
|
|
|
-- API functions
|
|
local healpredict = CreateFrame("Frame")
|
|
function healpredict.UnitGetIncomingHeals(unit, healer)
|
|
if UnitIsDeadOrGhost(unit) then return 0 end
|
|
|
|
local name = UnitName(unit)
|
|
|
|
if not heals[name] then
|
|
return 0
|
|
end
|
|
|
|
local sumheal, time = 0, GetTime()
|
|
|
|
for sender, amount in pairs(heals[name]) do
|
|
if amount[2] <= time then
|
|
heals[name][sender] = nil
|
|
elseif not healer or sender == healer then
|
|
sumheal = sumheal + amount[1]
|
|
end
|
|
end
|
|
|
|
return sumheal
|
|
end
|
|
|
|
function healpredict.RegisterCallback(addon, callback)
|
|
callbacks[addon] = callback
|
|
end
|
|
|
|
function healpredict.UnregisterCallback(addon)
|
|
callbacks[addon] = nil
|
|
end
|
|
|
|
-- Private functions
|
|
local function UpdateCache(spell, heal)
|
|
--[[
|
|
cache total of all heals and number of casts
|
|
for calculating a rolling average of the heal
|
|
]]
|
|
local heal = tonumber(heal)
|
|
|
|
if not cache[spell] then
|
|
cache[spell] = {heal, 1}
|
|
else
|
|
cache[spell][1] = cache[spell][1] + heal
|
|
cache[spell][2] = cache[spell][2] + 1
|
|
end
|
|
end
|
|
|
|
local function handleCallbacks(...)
|
|
for _, v in pairs(callbacks) do
|
|
v(...)
|
|
end
|
|
end
|
|
|
|
local function Heal(sender, target, amount, duration)
|
|
heals[target] = heals[target] or { }
|
|
heals[target][sender] = {amount, GetTime() + duration / 1000}
|
|
|
|
handleCallbacks(target)
|
|
end
|
|
|
|
local function HealStop(sender)
|
|
local affected = { }
|
|
for target, _ in pairs(heals) do
|
|
for tsender in pairs(heals[target]) do
|
|
if sender == tsender then
|
|
heals[target][tsender] = nil
|
|
table.insert(affected, target)
|
|
end
|
|
end
|
|
end
|
|
|
|
handleCallbacks(unpack(affected))
|
|
end
|
|
|
|
local function HealDelay(sender, delay)
|
|
if type(delay) ~= "string" then
|
|
local delay = delay / 1000
|
|
local affected = { }
|
|
for target, _ in pairs(heals) do
|
|
for tsender, amount in pairs(heals[target]) do
|
|
if sender == tsender then
|
|
amount[2] = amount[2] + delay
|
|
table.insert(affected, target)
|
|
end
|
|
end
|
|
end
|
|
handleCallbacks(unpack(affected))
|
|
end
|
|
end
|
|
|
|
local function SendHealMsg(msg)
|
|
SendAddonMessage(ADDON_NAME, msg, "RAID")
|
|
SendAddonMessage(ADDON_NAME, msg, "BATTLEGROUND")
|
|
end
|
|
|
|
local function max(targets)
|
|
local currentmax = -1
|
|
local raidname
|
|
|
|
for name, pct in pairs(targets) do
|
|
if pct > currentmax then
|
|
currentmax = pct
|
|
raidname = name
|
|
end
|
|
end
|
|
|
|
return raidname
|
|
end
|
|
|
|
local function BeaconTarget()
|
|
if beacon_info then
|
|
local beacon_target, endtime = unpack(beacon_info)
|
|
if endtime > GetTime() then
|
|
beacon_info = nil
|
|
else
|
|
return beacon_target
|
|
end
|
|
end
|
|
end
|
|
|
|
local function GroupHeal(amount, casttime)
|
|
local partyN, partyname, beacon_found
|
|
local beacon_target = BeaconTarget()
|
|
|
|
for i=1,4 do
|
|
partyN = "party"..i
|
|
|
|
if CheckInteractDistance(partyN, 4) then
|
|
partyname = (UnitName(partyN))
|
|
if beacon_target and partyname == beacon_target then
|
|
beacon_found = true
|
|
Heal(player, partyname, amount * 1.4, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, partyname, amount * 1.4, casttime))
|
|
elseif partyname then
|
|
Heal(player, partyname, amount, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, partyname, amount, casttime))
|
|
end
|
|
end
|
|
end
|
|
|
|
if beacon_target and not beacon_found then
|
|
Heal(player, beacon_target, amount * .4, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, beacon_target, amount * .4, casttime))
|
|
end
|
|
|
|
Heal(player, player, amount, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, player, amount, casttime))
|
|
end
|
|
|
|
local function SmartHeal(amount, casttime, n)
|
|
if not UnitInRaid("player") then
|
|
return GroupHeal(amount, casttime)
|
|
end
|
|
|
|
local beacon_target = BeaconTarget()
|
|
local beacon_found
|
|
|
|
local healthpct, currentmax
|
|
local pcts = { }
|
|
local raidN, raidname
|
|
|
|
for i=1,GetNumRaidMembers() do
|
|
raidN = "raid"..i
|
|
|
|
if not UnitIsDeadOrGhost(raidN) and CheckInteractDistance(raidN, 4) then
|
|
raidname = (UnitName(raidN))
|
|
healthpct = UnitHealth(raidN) / UnitHealthMax(raidname)
|
|
|
|
if #pcts < n then
|
|
pcts[raidname] = healthpct
|
|
|
|
if not currentmax or healthpct > pcts[currentmax] then
|
|
currentmax = raidname
|
|
end
|
|
|
|
elseif healthpct < pcts[currentmax] then
|
|
pcts[currentmax] = nil
|
|
pcts[raidname] = healthpct
|
|
currentmax = max(pcts)
|
|
end
|
|
end
|
|
end
|
|
|
|
for target, _ in pairs(pcts) do
|
|
if beacon_target and target == beacon_target then
|
|
beacon_found = true
|
|
Heal(player, target, amount * 1.4, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, target, amount * 1.4, casttime))
|
|
else
|
|
Heal(player, target, amount, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, target, amount, casttime))
|
|
end
|
|
end
|
|
|
|
if beacon_target and not beacon_found then
|
|
Heal(player, beacon_target, amount * .4, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, beacon_target, amount * .4, casttime))
|
|
end
|
|
end
|
|
|
|
local function UnitByName(name)
|
|
if name == player then
|
|
return "player"
|
|
end
|
|
|
|
local unit
|
|
|
|
if UnitInRaid("player") then
|
|
for i=1,GetNumRaidMembers() do
|
|
unit = "raid"..i
|
|
|
|
if (UnitName(unit)) == name then
|
|
return unit
|
|
end
|
|
end
|
|
end
|
|
|
|
for i=1,4 do
|
|
unit = "party"..i
|
|
|
|
if (UnitName(unit)) == name then
|
|
return unit
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Message passing
|
|
healpredict:RegisterEvent("CHAT_MSG_ADDON")
|
|
healpredict:SetScript("OnEvent", function(_, _, prefix, msg, _, sender)
|
|
if prefix == ADDON_NAME then
|
|
local command, target_or_delay, amount, casttime = strsplit(SEP, msg)
|
|
if command == HEALSTOP then
|
|
HealStop(sender)
|
|
elseif command == HEAL then
|
|
Heal(sender, target_or_delay, amount, casttime)
|
|
elseif command == HEALDELAY then
|
|
HealDelay(sender, target_or_delay)
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Reset cache on skill or inventory change
|
|
local resetcache = CreateFrame("Frame")
|
|
resetcache:RegisterEvent("SKILL_LINES_CHANGED")
|
|
resetcache:RegisterEvent("UNIT_INVENTORY_CHANGED")
|
|
resetcache:SetScript("OnEvent", function(_, event, player)
|
|
if player ~= "player" then
|
|
return
|
|
end
|
|
|
|
if event == "UNIT_INVENTORY_CHANGED" then
|
|
local gear = ""
|
|
for id = 1, 18 do
|
|
gear = gear .. (GetInventoryItemLink("player",id) or "")
|
|
end
|
|
|
|
if gear == gear_string then
|
|
return
|
|
end
|
|
|
|
gear_string = gear
|
|
end
|
|
|
|
-- reset cache
|
|
cache = { }
|
|
end)
|
|
|
|
--Event handling
|
|
----------------
|
|
local eventhandler = CreateFrame("Frame", ADDON_NAME .. "EventHandler", UIParent)
|
|
|
|
eventhandler:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
|
function eventhandler.COMBAT_LOG_EVENT_UNFILTERED(_, subevent, _, sourcename, _, _, destname, _, spellid, spellname, _, amount)
|
|
if sourcename ~= player then return end
|
|
|
|
if subevent == "SPELL_HEAL" then
|
|
local _, rank = GetSpellInfo(spellid)
|
|
local spellrank = spellname..(rank or "")
|
|
UpdateCache(spellrank, amount)
|
|
|
|
if spellname == TRANQUILITY then
|
|
-- Need to re-acquire tranq targets
|
|
HealStop(player)
|
|
SendHealMsg(HEALSTOP)
|
|
|
|
local _, _, _, _, starttime, endtime = UnitChannelInfo("player")
|
|
if starttime ~= nil and endtime ~= nil then
|
|
local casttime = endtime - starttime
|
|
|
|
local total, casts = unpack(cache[spellrank])
|
|
local amount = total / casts
|
|
|
|
SmartHeal(amount, casttime, 5)
|
|
is_healing = true
|
|
end
|
|
end
|
|
elseif spellname == BEACON_OF_LIGHT then
|
|
if subevent == "SPELL_AURA_APPLIED" then
|
|
local unit = UnitByName(destname)
|
|
|
|
if not unit then
|
|
return
|
|
end
|
|
|
|
for i=1,40 do
|
|
local buff, _, _, _, _, endtime = UnitBuff(unit, i)
|
|
if buff == BEACON_OF_LIGHT then
|
|
beacon_info = {destname, endtime}
|
|
break
|
|
end
|
|
end
|
|
|
|
elseif subevent == "SPELL_AURA_REMOVED" then
|
|
beacon_info = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_SENT")
|
|
function eventhandler.UNIT_SPELLCAST_SENT(unit, _, _, target)
|
|
if unit == "player" then
|
|
if target == "" then
|
|
current_target = UnitCanAssist("player", "target") and UnitName("target") or player
|
|
else
|
|
current_target = target
|
|
end
|
|
end
|
|
end
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_START")
|
|
function eventhandler.UNIT_SPELLCAST_START(unit)
|
|
if unit ~= "player" then return end
|
|
|
|
local spell, rank, _, _, starttime, endtime = UnitCastingInfo("player")
|
|
if not spell then
|
|
spell, rank, _, _, starttime, endtime = UnitChannelInfo("player")
|
|
end
|
|
local casttime = endtime - starttime
|
|
local spellrank = spell..(rank or "")
|
|
|
|
if cache[spellrank] then
|
|
local total, casts = unpack(cache[spellrank])
|
|
local amount = total / casts
|
|
|
|
if spell == PRAYER_OF_HEALING then
|
|
GroupHeal(amount, casttime)
|
|
elseif SMART_HEALS[spell] then
|
|
SmartHeal(amount, casttime, SMART_HEALS[spell])
|
|
else
|
|
local beacon_target = BeaconTarget()
|
|
|
|
if beacon_target then
|
|
if beacon_target ~= current_target then
|
|
Heal(player, current_target, amount, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, current_target, amount, casttime))
|
|
|
|
Heal(player, beacon_target, amount * .4, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, beacon_target, amount * .4, casttime))
|
|
else
|
|
Heal(player, beacon_target, amount * 1.4, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, beacon_target, amount * 1.4, casttime))
|
|
end
|
|
else
|
|
Heal(player, current_target, amount, casttime)
|
|
SendHealMsg(strjoin(SEP, HEAL, current_target, amount, casttime))
|
|
end
|
|
end
|
|
|
|
is_healing = true
|
|
end
|
|
end
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
|
eventhandler.UNIT_SPELLCAST_CHANNEL_START = eventhandler.UNIT_SPELLCAST_START
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_FAILED")
|
|
function eventhandler.UNIT_SPELLCAST_FAILED(unit)
|
|
if is_healing and unit == "player" then
|
|
HealStop(player)
|
|
SendHealMsg(HEALSTOP)
|
|
is_healing = nil
|
|
end
|
|
end
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
|
|
eventhandler.UNIT_SPELLCAST_INTERRUPTED = eventhandler.UNIT_SPELLCAST_FAILED
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_STOP")
|
|
eventhandler.UNIT_SPELLCAST_STOP = eventhandler.UNIT_SPELLCAST_FAILED
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
|
eventhandler.UNIT_SPELLCAST_CHANNEL_STOP = eventhandler.UNIT_SPELLCAST_FAILED
|
|
|
|
eventhandler:RegisterEvent("UNIT_SPELLCAST_DELAYED")
|
|
function eventhandler.UNIT_SPELLCAST_DELAYED(unit, delay)
|
|
if is_healing and unit == "player" then
|
|
HealDelay(player, delay)
|
|
SendHealMsg(strjoin(SEP, HEALDELAY, delay))
|
|
end
|
|
end
|
|
|
|
eventhandler:SetScript("OnEvent", function(_, event, ...)
|
|
local handler = eventhandler[event]
|
|
if handler then
|
|
handler(...)
|
|
end
|
|
end)
|
|
|
|
_G[ADDON_NAME] = healpredict |