Merge branch 'heal-prediction'

This commit is contained in:
andrew6180
2025-10-26 11:26:36 -07:00
5 changed files with 9 additions and 3538 deletions
-542
View File
@@ -1,542 +0,0 @@
--[[
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
-1
View File
@@ -18,7 +18,6 @@
<!-- -->
<Script file="LibSimpleSticky\LibSimpleSticky.lua"/>
<Script file="LibSpellRange-1.0\LibSpellRange-1.0.lua"/>
<Script file="HealPredict\healpredict.lua"/>
<Include file="oUF\oUF.xml"/>
<Include file="oUF_Plugins\oUF_Plugins.xml"/>
<Script file="LibDataBroker\LibDataBroker-1.1.lua"/>
File diff suppressed because it is too large Load Diff
@@ -49,12 +49,10 @@ local _, ns = ...
local oUF = ns.oUF or oUF
assert(oUF, "oUF_HealComm4 was unable to locate oUF install")
local healpredict = HealPredict
assert(healpredict, "oUF_HealComm4 was unable to locate HealPredict")
local UnitHealth = UnitHealth
local UnitHealthMax = UnitHealthMax
local UnitName = UnitName
local UnitGetIncomingHeals = UnitGetIncomingHeals
local enabledUF, enabled = {}, nil
@@ -72,8 +70,8 @@ local function Update(self)
element:PreUpdate(unit)
end
local myIncomingHeal = healpredict.UnitGetIncomingHeals(unit, UnitName("player")) or 0
local allIncomingHeal = healpredict.UnitGetIncomingHeals(unit) or 0
local myIncomingHeal = UnitGetIncomingHeals(unit, UnitName("player")) or 0
local allIncomingHeal = UnitGetIncomingHeals(unit) or 0
local health = UnitHealth(unit)
local maxHealth = UnitHealthMax(unit)
local maxOverflowHP = maxHealth * element.maxOverflow
@@ -146,18 +144,6 @@ local function UpdateAllUnits(...)
end
end
local function ToggleCallbacks(toggle)
if toggle and not enabled and #enabledUF > 0 then
healpredict.RegisterCallback("oUF_HealComm", UpdateAllUnits)
enabled = true
elseif not toggle and enabled and #enabledUF == 0 then
healpredict.UnregisterCallback("oUF_HealComm")
enabled = nil
end
end
local function Enable(self)
local element = self.HealCommBar
@@ -167,6 +153,7 @@ local function Enable(self)
self:RegisterEvent("UNIT_HEALTH", Path)
self:RegisterEvent("UNIT_MAXHEALTH", Path)
self:RegisterEvent("UNIT_HEAL_PREDICTION", Path)
if not element.maxOverflow then
element.maxOverflow = 1.05
@@ -181,8 +168,6 @@ local function Enable(self)
end
enabledUF[#enabledUF + 1] = self
ToggleCallbacks(true)
return true
end
end
@@ -201,15 +186,7 @@ local function Disable(self)
self:UnregisterEvent("UNIT_HEALTH", Path)
self:UnregisterEvent("UNIT_MAXHEALTH", Path)
for i = 1, #enabledUF do
if enabledUF[i] == self then
table.remove(enabledUF, i)
break
end
end
ToggleCallbacks(false)
self:UnregisterEvent("UNIT_HEAL_PREDICTION", Path)
end
end
+4 -4
View File
@@ -2792,7 +2792,7 @@ P.unitframe = {
startFromCenter = false,
showPlayer = true,
healPrediction = {
enable = false
enable = true
},
colorOverride = "USE_DEFAULT",
width = 184,
@@ -3061,7 +3061,7 @@ P.unitframe = {
sortDir = "ASC",
showPlayer = true,
healPrediction = {
enable = false
enable = true
},
colorOverride = "USE_DEFAULT",
width = 80,
@@ -3258,7 +3258,7 @@ P.unitframe = {
sortDir = "ASC",
showPlayer = true,
healPrediction = {
enable = false
enable = true
},
colorOverride = "USE_DEFAULT",
width = 80,
@@ -3457,7 +3457,7 @@ P.unitframe = {
invertGroupingOrder = false,
startFromCenter = false,
healPrediction = {
enable = true
enable = false
},
colorOverride = "USE_DEFAULT",
width = 80,