From aa73e60372e58514ad10465f07248ff0aaa5e5c9 Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:35:56 +0100 Subject: [PATCH 1/7] prayer of healing fix - now always predicts the priests group/target --- libs/libpredict.lua | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/libs/libpredict.lua b/libs/libpredict.lua index da298a59..bfe1d9e2 100644 --- a/libs/libpredict.lua +++ b/libs/libpredict.lua @@ -241,18 +241,29 @@ pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGu end if spellName == PRAYER_OF_HEALING then - target = player + -- target is already correctly set from spell_queue[3]: + -- selfcast (ALT) = player, otherwise = current target + -- Use this to find the correct group to heal + local pohTarget = target or player if GetNumRaidMembers() > 0 then - -- Raid: find our subgroup and heal only those members - local myGroup + -- Raid: find pohTarget's subgroup and heal only those members + -- (Turtle WoW changed PoH to heal the target's group, not the caster's group) + local targetGroup for i = 1, GetNumRaidMembers() do local name, _, subgroup = GetRaidRosterInfo(i) - if name == player then myGroup = subgroup break end + if name == pohTarget then targetGroup = subgroup break end end - if myGroup then + -- Fallback to caster's own group if target not in raid + if not targetGroup then for i = 1, GetNumRaidMembers() do local name, _, subgroup = GetRaidRosterInfo(i) - if subgroup == myGroup and name ~= player then + if name == player then targetGroup = subgroup break end + end + end + if targetGroup then + for i = 1, GetNumRaidMembers() do + local name, _, subgroup = GetRaidRosterInfo(i) + if subgroup == targetGroup then libpredict:Heal(player, name, amount, casttime) libpredict.sender:SendHealCommMsg("Heal/" .. name .. "/" .. amount .. "/" .. casttime .. "/") libpredict.sender.healing = true @@ -260,7 +271,10 @@ pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGu end end else - -- Party: heal party1-4 + -- Party: heal all party members including self + libpredict:Heal(player, player, amount, casttime) + libpredict.sender:SendHealCommMsg("Heal/" .. player .. "/" .. amount .. "/" .. casttime .. "/") + libpredict.sender.healing = true for i = 1, 4 do if CheckInteractDistance("party"..i, 4) then local pname = UnitName("party"..i) @@ -270,6 +284,7 @@ pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGu end end end + return -- skip the generic Heal call below end libpredict:Heal(player, target, amount, casttime) From 5869c9f5190713db704fb126e61e077f7de1dd72 Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:02:22 +0100 Subject: [PATCH 2/7] fixed macrotweak to check properly for scrm, sm and um --- modules/macrotweak.lua | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/modules/macrotweak.lua b/modules/macrotweak.lua index 3d45d862..3ed5414c 100644 --- a/modules/macrotweak.lua +++ b/modules/macrotweak.lua @@ -1,12 +1,24 @@ pfUI:RegisterModule("macrotweak", "vanilla", function () - -- disable macrotweak when macro addons are loaded - if IsAddOnLoaded("Supermacro") or IsAddOnLoaded("SuperCleveRoidMacros") or IsAddOnLoaded("UltimaMacros") then return end + local conflictAddons = { "Supermacro", "SuperCleveRoidMacros", "UltimaMacros" } + local disabled = false + + local function CheckConflicts() + for _, name in pairs(conflictAddons) do + if IsAddOnLoaded(name) then + DEFAULT_CHAT_FRAME:AddMessage("|cff33ffccpfUI|r: " .. name .. " found, macrotweak disabled.") + disabled = true + return true + end + end + return false + end -- do not write macro calls into chat input history if ChatFrameEditBox._AddHistoryLine then local userinput ChatFrameEditBox._AddHistoryLine = ChatFrameEditBox.AddHistoryLine ChatFrameEditBox.AddHistoryLine = function(self, text) + if disabled then return ChatFrameEditBox._AddHistoryLine(self, text) end if not userinput and text and string.find(text, "^/run(.+)") then return end if not userinput and string.find(text, "^/script(.+)") then return end if not userinput and string.find(text, "^/cast(.+)") then return end @@ -24,6 +36,7 @@ pfUI:RegisterModule("macrotweak", "vanilla", function () -- make sure #showtooltip inside macros won't be sent local hookSendChatMessage = SendChatMessage function _G.SendChatMessage(msg, ...) + if disabled then return hookSendChatMessage(msg, unpack(arg)) end if msg and string.find(msg, "^#showtooltip ") then return end hookSendChatMessage(msg, unpack(arg)) end @@ -51,4 +64,11 @@ pfUI:RegisterModule("macrotweak", "vanilla", function () UseInventoryItem(slot) end end) -end) + + -- Check conflicts after one tick so all addons have finished loading + local watcher = CreateFrame("Frame") + watcher:SetScript("OnUpdate", function() + this:SetScript("OnUpdate", nil) + CheckConflicts() + end) +end) \ No newline at end of file From fcae37f9b0929fe12454a575d7a02fd8ab53e8c0 Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:35:19 +0100 Subject: [PATCH 3/7] force reload for sp settings --- modules/gui.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/gui.lua b/modules/gui.lua index f87dad94..5a7b491c 100644 --- a/modules/gui.lua +++ b/modules/gui.lua @@ -2392,16 +2392,17 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function () CreateConfig(U[c], T["Focus Color"], C.unitframes[c], "focuscolor", "color") if c == "player" then + local function spHasteReload() if U[c] then U[c]() end Reload() end CreateConfig(U[c], T["Player SP/Haste Display"], nil, nil, "header") - CreateConfig(U[c], T["Haste Display"], C.unitframes[c], "display_haste", "dropdown", { + CreateConfig(spHasteReload, T["Haste Display"], C.unitframes[c], "display_haste", "dropdown", { "0:"..T["None"], "1:"..T["Haste (cast speed increase)"], "2:"..T["Effective Haste (Haste * cast time reduction)"], -- Only affects mages/warlocks I believe }) - CreateConfig(U[c], T["Haste Display Color"], C.unitframes[c], "display_haste_color", "color") - CreateConfig(U[c], T["Display Spell Power"], C.unitframes[c], "display_spellpower", "checkbox") - CreateConfig(U[c], T["Use Custom Spell Power Color (unchecked = biggest school color)"], C.unitframes[c], "display_sp_color_override", "checkbox") - CreateConfig(U[c], T["Spell Power Color"], C.unitframes[c], "display_sp_color", "color") + CreateConfig(spHasteReload, T["Haste Display Color"], C.unitframes[c], "display_haste_color", "color") + CreateConfig(spHasteReload, T["Display Spell Power"], C.unitframes[c], "display_spellpower", "checkbox") + CreateConfig(spHasteReload, T["Use Custom Spell Power Color (unchecked = biggest school color)"], C.unitframes[c], "display_sp_color_override", "checkbox") + CreateConfig(spHasteReload, T["Spell Power Color"], C.unitframes[c], "display_sp_color", "color") end end) end From bfe18170bfb564250e38bb3c7210db0e84bf9b9b Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:36:03 +0100 Subject: [PATCH 4/7] Revert "force reload for sp settings" This reverts commit fcae37f9b0929fe12454a575d7a02fd8ab53e8c0. --- modules/gui.lua | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/modules/gui.lua b/modules/gui.lua index 5a7b491c..f87dad94 100644 --- a/modules/gui.lua +++ b/modules/gui.lua @@ -2392,17 +2392,16 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function () CreateConfig(U[c], T["Focus Color"], C.unitframes[c], "focuscolor", "color") if c == "player" then - local function spHasteReload() if U[c] then U[c]() end Reload() end CreateConfig(U[c], T["Player SP/Haste Display"], nil, nil, "header") - CreateConfig(spHasteReload, T["Haste Display"], C.unitframes[c], "display_haste", "dropdown", { + CreateConfig(U[c], T["Haste Display"], C.unitframes[c], "display_haste", "dropdown", { "0:"..T["None"], "1:"..T["Haste (cast speed increase)"], "2:"..T["Effective Haste (Haste * cast time reduction)"], -- Only affects mages/warlocks I believe }) - CreateConfig(spHasteReload, T["Haste Display Color"], C.unitframes[c], "display_haste_color", "color") - CreateConfig(spHasteReload, T["Display Spell Power"], C.unitframes[c], "display_spellpower", "checkbox") - CreateConfig(spHasteReload, T["Use Custom Spell Power Color (unchecked = biggest school color)"], C.unitframes[c], "display_sp_color_override", "checkbox") - CreateConfig(spHasteReload, T["Spell Power Color"], C.unitframes[c], "display_sp_color", "color") + CreateConfig(U[c], T["Haste Display Color"], C.unitframes[c], "display_haste_color", "color") + CreateConfig(U[c], T["Display Spell Power"], C.unitframes[c], "display_spellpower", "checkbox") + CreateConfig(U[c], T["Use Custom Spell Power Color (unchecked = biggest school color)"], C.unitframes[c], "display_sp_color_override", "checkbox") + CreateConfig(U[c], T["Spell Power Color"], C.unitframes[c], "display_sp_color", "color") end end) end From c746f391d0873a1cf0b6630df0afeb814b7e5fd7 Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:31:26 +0100 Subject: [PATCH 5/7] forced reload for spellpower display --- modules/gui.lua | 14 +++++++------- modules/player.lua | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/modules/gui.lua b/modules/gui.lua index f87dad94..72013edf 100644 --- a/modules/gui.lua +++ b/modules/gui.lua @@ -25,7 +25,7 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function () ufunc = function() for _, fr in ipairs(pfUI.uf.frames) do if fr.label == key then - pfUI.uf.UpdateConfig(fr) + fr:UpdateConfig() end end end @@ -2392,16 +2392,16 @@ pfUI:RegisterModule("gui", "vanilla:tbc", function () CreateConfig(U[c], T["Focus Color"], C.unitframes[c], "focuscolor", "color") if c == "player" then - CreateConfig(U[c], T["Player SP/Haste Display"], nil, nil, "header") - CreateConfig(U[c], T["Haste Display"], C.unitframes[c], "display_haste", "dropdown", { + CreateConfig(nil, T["Player SP/Haste Display"], nil, nil, "header") + CreateConfig(nil, T["Haste Display"], C.unitframes[c], "display_haste", "dropdown", { "0:"..T["None"], "1:"..T["Haste (cast speed increase)"], "2:"..T["Effective Haste (Haste * cast time reduction)"], -- Only affects mages/warlocks I believe }) - CreateConfig(U[c], T["Haste Display Color"], C.unitframes[c], "display_haste_color", "color") - CreateConfig(U[c], T["Display Spell Power"], C.unitframes[c], "display_spellpower", "checkbox") - CreateConfig(U[c], T["Use Custom Spell Power Color (unchecked = biggest school color)"], C.unitframes[c], "display_sp_color_override", "checkbox") - CreateConfig(U[c], T["Spell Power Color"], C.unitframes[c], "display_sp_color", "color") + CreateConfig(nil, T["Haste Display Color"], C.unitframes[c], "display_haste_color", "color") + CreateConfig(nil, T["Display Spell Power"], C.unitframes[c], "display_spellpower", "checkbox") + CreateConfig(nil, T["Use Custom Spell Power Color (unchecked = biggest school color)"], C.unitframes[c], "display_sp_color_override", "checkbox") + CreateConfig(nil, T["Spell Power Color"], C.unitframes[c], "display_sp_color", "color") end end) end diff --git a/modules/player.lua b/modules/player.lua index b9f3434e..a4ff84af 100644 --- a/modules/player.lua +++ b/modules/player.lua @@ -142,7 +142,11 @@ pfUI:RegisterModule("player", "vanilla:tbc", function () playerFrame.infoTopCenterText:SetText(text) end + -- Keep a reference to the generic UF UpdateConfig so we can chain it + local genericUpdateConfig = pfUI.uf.UpdateConfig + function playerFrame:UpdateConfig() + genericUpdateConfig(self) UpdateInfoText() end From 80fe64e9971495396db82ea195da0c6c715988cf Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:40:58 +0100 Subject: [PATCH 6/7] advanced libpredict --- libs/libpredict.lua | 86 ++++++++++++++++++++++++++++++++------------- 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/libs/libpredict.lua b/libs/libpredict.lua index bfe1d9e2..36bbb82b 100644 --- a/libs/libpredict.lua +++ b/libs/libpredict.lua @@ -40,7 +40,7 @@ local cache, gear_string = {}, "" local rejuvDuration, renewDuration = 12, 15 --default durations local ressGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER cleanup local healGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER cleanup -local ress_timers = {} -- [target][sender] = expiry_timestamp (60s Rez-Fenster) +local ress_timers = {} -- [target][sender] = expiry_timestamp (60s rez window) local RESS_TIMEOUT = 60 -- Vanilla: rez offer expires after 60s local PRAYER_OF_HEALING @@ -176,7 +176,7 @@ local function isRezSpell(spellId) return spellName and L["resurrections"][spellName] end --- Cache beim Betreten der Welt aufbauen +-- Build cache on world enter libpredict:RegisterEvent("PLAYER_ENTERING_WORLD") libpredict:RegisterEvent("RAID_ROSTER_UPDATE") libpredict:RegisterEvent("PARTY_MEMBERS_CHANGED") @@ -188,7 +188,7 @@ libpredict:SetScript("OnEvent", function() if origOnEvent then origOnEvent() end end) --- SPELL_START_SELF: eigener Cast gestartet (Heals + Rez) +-- SPELL_START_SELF: own cast started (heals + rez) pfUI.libdebuff_spell_start_self_hooks = pfUI.libdebuff_spell_start_self_hooks or {} pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGuid, targetGuid, castTime) local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") @@ -293,7 +293,7 @@ pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGu end end --- SPELL_GO_SELF: eigener Cast gelandet (HealStop + Regrowth timer) +-- SPELL_GO_SELF: own cast landed (HealStop + Regrowth timer) pfUI.libdebuff_spell_go_hooks["libpredict_sender"] = function(spellId) libpredict:HealStop(player) local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") @@ -310,19 +310,50 @@ pfUI.libdebuff_spell_go_hooks["libpredict_sender"] = function(spellId) libpredict.sender.current_cast_target = nil end --- SPELL_START_OTHER: fremder Rez-Cast gestartet +-- SPELL_START_OTHER: foreign cast started (heals + rez) -- Signature: fn(spellId, casterGuid, targetGuid, castTime) pfUI.libdebuff_spell_start_other_hooks = pfUI.libdebuff_spell_start_other_hooks or {} pfUI.libdebuff_spell_start_other_hooks["libpredict"] = function(spellId, casterGuid, targetGuid, castTime) - if not isRezSpell(spellId) then return end + local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + if not spellName then return end + local casterName = resolveNameFromGuid(casterGuid) - local targetName = resolveNameFromGuid(targetGuid) - if casterName and targetName then - libpredict:Ress(casterName, targetName, casterGuid) + if not casterName then return end + + -- Resurrection cast + if L["resurrections"][spellName] then + local targetName = resolveNameFromGuid(targetGuid) + if targetName then + libpredict:Ress(casterName, targetName, casterGuid) + end + return end + + -- Heal: use GetSpellRec base values as estimate (no SP/talents — rough) + local targetName = resolveNameFromGuid(targetGuid) + if not targetName then return end + + local amount + if GetSpellRec then + local rec = GetSpellRec(spellId) + if rec then + local base = rec.effectBasePoints1 + local dice = rec.effectDieSides1 + if not base and rec.effectBasePoints then + base = rec.effectBasePoints[1] + dice = rec.effectDieSides and rec.effectDieSides[1] or 0 + end + if base then + amount = (base + 1) + math.floor((dice or 0) / 2) + end + end + end + if not amount or amount <= 0 then return end + + libpredict:Heal(casterName, targetName, amount, castTime, casterGuid) end --- SPELL_GO_SELF: eigener Cast gelandet - HoTs + eigener Rez Timer +-- SPELL_GO_SELF: own cast landed - HoTs + own rez timer -- Signature: fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7) pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {} pfUI.libdebuff_spell_go_hooks["libpredict"] = function(spellId, a1, a2, a3, a4, a5, a6, a7) @@ -355,7 +386,7 @@ pfUI.libdebuff_spell_go_hooks["libpredict"] = function(spellId, a1, a2, a3, a4, end end end - -- Eigener Rez gelandet: Timer setzen + -- Own rez landed: set timer if isRezSpell(spellId) then local targetGuid = a4 local targetName = resolveNameFromGuid(targetGuid) @@ -367,19 +398,26 @@ pfUI.libdebuff_spell_go_hooks["libpredict"] = function(spellId, a1, a2, a3, a4, end --- SPELL_GO_OTHER: fremder Rez gelandet - Timer setzen +-- SPELL_GO_OTHER: foreign cast landed - rez timer + HealStop -- Signature: fn(spellId, casterGuid, targetGuid) pfUI.libdebuff_spell_go_other_hooks = pfUI.libdebuff_spell_go_other_hooks or {} pfUI.libdebuff_spell_go_other_hooks["libpredict"] = function(spellId, casterGuid, targetGuid) - if not isRezSpell(spellId) then return end local casterName = resolveNameFromGuid(casterGuid) - local targetName = resolveNameFromGuid(targetGuid) - if casterName and targetName then - libpredict:RessSetTimer(casterName, targetName) + if not casterName then return end + + if isRezSpell(spellId) then + local targetName = resolveNameFromGuid(targetGuid) + if targetName then + libpredict:RessSetTimer(casterName, targetName) + end + return end + + -- Heal landed → prediction fulfilled, clean up + libpredict:HealStop(casterName) end --- SPELL_FAILED_OTHER: abgebrochene Resses/Heals entfernen +-- SPELL_FAILED_OTHER: remove cancelled resses/heals -- Signature: fn(casterGuid, spellId) pfUI.libdebuff_spell_failed_other_hooks = pfUI.libdebuff_spell_failed_other_hooks or {} pfUI.libdebuff_spell_failed_other_hooks["libpredict"] = function(casterGuid, spellId) @@ -405,7 +443,7 @@ libpredict:SetScript("OnUpdate", function() end end - -- Rez-Timeout: Angebot nach 60s entfernen wenn Spieler nicht annimmt + -- Rez timeout: remove offer after 60s if player doesn't accept for target, senders in pairs(ress_timers) do for sender, expiry in pairs(senders) do if now >= expiry then @@ -549,7 +587,7 @@ function libpredict:ParseChatMessage(sender, msg, comm) libpredict:Ress(sender, target, senderGuid) end elseif msgtype == "Hot" then - -- Duplikat-Check: gleicher sender+target+spell innerhalb DUPLICATE_WINDOW ignorieren + -- Duplicate check: ignore same sender+target+spell within DUPLICATE_WINDOW local now = pfUI.uf.now or GetTime() local key = sender .. target .. heal if recentHots[key] and (now - recentHots[key]) < DUPLICATE_WINDOW then @@ -700,7 +738,7 @@ function libpredict:Ress(sender, target, senderGuid) ress[target] = ress[target] or {} ress[target][sender] = true if senderGuid then ressGuidToName[senderGuid] = sender end - -- kein Timer hier - Timer wird erst gesetzt wenn Cast wirklich durchgeht + -- no timer here - timer is only set once the cast actually completes end function libpredict:RessSetTimer(sender, target) @@ -884,7 +922,7 @@ hooksecurefunc("CastSpell", function(id, bookType) rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil end - -- Instant-HoTs: libdebuff/Nampower via GetHotDuration, Hook-Methode als Fallback + -- Instant HoTs: libdebuff/Nampower via GetHotDuration, hook method as fallback if effect == REJUVENATION then local target = spell_queue[3] @@ -950,7 +988,7 @@ hooksecurefunc("CastSpellByName", function(effect, target) spell_queue[3] = target or mouseover or default end - -- Instant-HoTs: libdebuff/Nampower via GetHotDuration, Hook-Methode als Fallback + -- Instant HoTs: libdebuff/Nampower via GetHotDuration, hook method as fallback if effect == REJUVENATION then local hotTarget = target or mouseover or default @@ -1006,7 +1044,7 @@ hooksecurefunc("UseAction", function(slot, target, selfcast) rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil end - -- Instant-HoTs: libdebuff/Nampower via GetHotDuration, Hook-Methode als Fallback + -- Instant HoTs: libdebuff/Nampower via GetHotDuration, hook method as fallback if effect == REJUVENATION then local hotTarget = spell_queue[3] @@ -1236,7 +1274,7 @@ _G.SlashCmdList.HOTDEBUG = function() end DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[FALLBACK]|r Legacy prediction system: ACTIVE") - DEFAULT_CHAT_FRAME:AddMessage(" Using Hook-Methode + HealComm messages") + DEFAULT_CHAT_FRAME:AddMessage(" Using hook method + HealComm messages") -- Show active HoTs in tracking local hotCount = 0 From b041a167442dcd7bd628f6f4800a3e0f3d300a7d Mon Sep 17 00:00:00 2001 From: Meow <30401521+me0wg4ming@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:57:47 +0100 Subject: [PATCH 7/7] Update libpredict.lua --- libs/libpredict.lua | 87 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/libs/libpredict.lua b/libs/libpredict.lua index 36bbb82b..b3508045 100644 --- a/libs/libpredict.lua +++ b/libs/libpredict.lua @@ -37,6 +37,7 @@ local heals, ress, events, hots = {}, {}, {}, {} local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" } local player = UnitName("player") local cache, gear_string = {}, "" +local foreignCache = {} -- [casterName][spellKey] = amount, in-memory cache for other healers local rejuvDuration, renewDuration = 12, 15 --default durations local ressGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER cleanup local healGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER cleanup @@ -329,26 +330,50 @@ pfUI.libdebuff_spell_start_other_hooks["libpredict"] = function(spellId, casterG return end - -- Heal: use GetSpellRec base values as estimate (no SP/talents — rough) + -- Heal cast: look up cached amount from a previous cast by this healer local targetName = resolveNameFromGuid(targetGuid) if not targetName then return end - local amount - if GetSpellRec then - local rec = GetSpellRec(spellId) - if rec then - local base = rec.effectBasePoints1 - local dice = rec.effectDieSides1 - if not base and rec.effectBasePoints then - base = rec.effectBasePoints[1] - dice = rec.effectDieSides and rec.effectDieSides[1] or 0 + local rankStr = GetSpellRecField and GetSpellRecField(spellId, "rank") or "" + local spellKey = spellName .. (rankStr or "") + + local amount = foreignCache[casterName] and foreignCache[casterName][spellKey] + if not amount then return end -- no data yet, skip until we have a real value + + -- Prayer of Healing: heal entire subgroup of the target + if spellName == PRAYER_OF_HEALING then + if GetNumRaidMembers() > 0 then + local targetGroup + for i = 1, GetNumRaidMembers() do + local rname, _, subgroup = GetRaidRosterInfo(i) + if rname == targetName then targetGroup = subgroup break end end - if base then - amount = (base + 1) + math.floor((dice or 0) / 2) + if not targetGroup then + for i = 1, GetNumRaidMembers() do + local rname, _, subgroup = GetRaidRosterInfo(i) + if rname == casterName then targetGroup = subgroup break end + end + end + if targetGroup then + for i = 1, GetNumRaidMembers() do + local rname, _, subgroup = GetRaidRosterInfo(i) + if subgroup == targetGroup then + libpredict:Heal(casterName, rname, amount, castTime, casterGuid) + end + end + end + else + -- Party: heal self + all members + libpredict:Heal(casterName, casterName, amount, castTime, casterGuid) + for i = 1, 4 do + local pname = UnitName("party" .. i) + if pname then + libpredict:Heal(casterName, pname, amount, castTime, casterGuid) + end end end + return end - if not amount or amount <= 0 then return end libpredict:Heal(casterName, targetName, amount, castTime, casterGuid) end @@ -1138,6 +1163,7 @@ end) libpredict.sender:RegisterEvent("SPELL_FAILED_SELF") libpredict.sender:RegisterEvent("SPELL_DELAYED_SELF") libpredict.sender:RegisterEvent("SPELL_HEAL_BY_SELF") +libpredict.sender:RegisterEvent("SPELL_HEAL_BY_OTHER") -- populates foreignCache for other healers -- force cache updates libpredict.sender:RegisterEvent("UNIT_INVENTORY_CHANGED") @@ -1182,6 +1208,41 @@ libpredict.sender:SetScript("OnEvent", function() UpdateCache(spell_queue[2], amount, isCrit) end + elseif event == "SPELL_HEAL_BY_OTHER" then + -- Fires once per hit target for AoE heals (e.g. PoH). + -- Use this to build a per-caster cache of real heal amounts. + -- arg1=targetGuid, arg2=casterGuid, arg3=spellId, arg4=amount, arg5=critical, arg6=periodic + local casterGuid = arg2 + local spellId = arg3 + local amount = arg4 + local isCrit = arg5 == 1 + local isPeriodic = arg6 == 1 + + if isPeriodic or not spellId or not amount or amount <= 0 then return end + + local casterName = resolveNameFromGuid(casterGuid) + if not casterName or casterName == player then return end -- own heals handled by SPELL_HEAL_BY_SELF + + local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + if not spellName then return end + local rankStr = GetSpellRecField and GetSpellRecField(spellId, "rank") or "" + local spellKey = spellName .. (rankStr or "") + + foreignCache[casterName] = foreignCache[casterName] or {} + local existing = foreignCache[casterName][spellKey] + -- Store highest non-crit value for best prediction accuracy + if isCrit then + -- Estimate base from crit (vanilla crit = 150%) + local base = math.floor(amount * 2 / 3) + if not existing or base > existing then + foreignCache[casterName][spellKey] = base + end + else + if not existing or amount > existing then + foreignCache[casterName][spellKey] = amount + end + end + elseif event == "SPELL_FAILED_SELF" then onCastFailed()