refactor: implement reliable shapeshift unshifting with a queued cast system and Nampower integration settings

This commit is contained in:
Bluewhale1337
2026-09-01 18:55:47 +02:00
parent 59ceb3b465
commit 16bc4d95ee
5 changed files with 109 additions and 33 deletions
+53 -9
View File
@@ -78,23 +78,37 @@ function HealBot_OnUpdate(this, arg1)
end end
if HealBot_PendingShapeshiftCast then if HealBot_PendingShapeshiftCast then
if GetTime() > HealBot_PendingShapeshiftCast.expires then
HealBot_PendingShapeshiftCast = nil
else
-- Only cast once the form has successfully been removed
local currentForm = HealBot_GetShapeshiftForm()
if not currentForm then
local pendingCast = HealBot_PendingShapeshiftCast local pendingCast = HealBot_PendingShapeshiftCast
HealBot_PendingShapeshiftCast = nil
-- Wait for server to process the unshift before attempting the cast
if GetTime() >= pendingCast.fireTime then
if not HealBot_GetShapeshiftForm() then
-- Target the unit if necessary before casting
-- Target the unit if necessary before casting -- Target the unit if necessary before casting
if pendingCast.oldTarget ~= UnitName(pendingCast.target) then if pendingCast.oldTarget ~= UnitName(pendingCast.target) then
TargetUnit(pendingCast.target) TargetUnit(pendingCast.target)
end end
-- Attempt the cast now that form is cleared -- 1. Initialize the MVC side effects (AnnounceCast, Incoming Heals) only ONCE
HealBot_StartCasting(pendingCast.spell, pendingCast.target, "direct") if not pendingCast.started then
pendingCast.started = true
-- Extract base spell
local baseSpell = pendingCast.spell
local parenIndex = string.find(pendingCast.spell, "%(")
if parenIndex then
baseSpell = string.sub(pendingCast.spell, 1, parenIndex - 1)
end
baseSpell = string.gsub(baseSpell, "%s+$", "")
-- Force MVC updates since we guarantee the cast will eventually pierce the server
HealBot_CastFailed = false
HealBot_CastingSpell = baseSpell
HealBot_CastingTarget = pendingCast.target
HealBot_Process_HealValue(baseSpell, pendingCast.target)
HealBot_AnnounceCast(pendingCast.spell, pendingCast.target)
-- Restore target logic
if pendingCast.targetEnemy then if pendingCast.targetEnemy then
HealBot_TargetRestorePending = { type = "enemy" } HealBot_TargetRestorePending = { type = "enemy" }
elseif pendingCast.oldTarget and pendingCast.oldTarget ~= UnitName(pendingCast.target) then elseif pendingCast.oldTarget and pendingCast.oldTarget ~= UnitName(pendingCast.target) then
@@ -104,6 +118,34 @@ function HealBot_OnUpdate(this, arg1)
end end
HealBot_TargetRestoreTimer = 0 HealBot_TargetRestoreTimer = 0
end end
-- 2. Spam the raw cast silently to guarantee it pierces the server delay
if pendingCast.started then
if not pendingCast.nextSpam or GetTime() >= pendingCast.nextSpam then
pendingCast.nextSpam = GetTime() + 0.15
-- Safely suppress UI errors
UIErrorsFrame:UnregisterEvent("UI_ERROR_MESSAGE")
-- Only use HealBot's native cast wrapper
HealBot_CastSpellByName(pendingCast.spell)
if SpellCanTargetUnit(pendingCast.target) then
SpellTargetUnit(pendingCast.target)
elseif SpellIsTargeting() then
SpellTargetUnit(pendingCast.target)
SpellStopTargeting()
end
UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE")
end
end
end
end
-- Failsafe timeout
if HealBot_PendingShapeshiftCast and GetTime() > pendingCast.expires then
HealBot_PendingShapeshiftCast = nil
end end
end end
@@ -524,6 +566,7 @@ end
-- HealBot_OnEvent_SpellcastStart: Internal utility: HealBot_OnEvent_SpellcastStart -- HealBot_OnEvent_SpellcastStart: Internal utility: HealBot_OnEvent_SpellcastStart
function HealBot_OnEvent_SpellcastStart(this, spell, duration) function HealBot_OnEvent_SpellcastStart(this, spell, duration)
HealBot_IsCasting = true; HealBot_IsCasting = true;
HealBot_PendingShapeshiftCast = nil;
HealBot_RecalcHeals(); HealBot_RecalcHeals();
HealBot_CheckCasting(); HealBot_CheckCasting();
if spell == HEALBOT_RESURRECTION or spell == HEALBOT_ANCESTRALSPIRIT or spell == HEALBOT_REBIRTH or spell == HEALBOT_REDEMPTION then if spell == HEALBOT_RESURRECTION or spell == HEALBOT_ANCESTRALSPIRIT or spell == HEALBOT_REBIRTH or spell == HEALBOT_REDEMPTION then
@@ -537,6 +580,7 @@ end
-- HealBot_OnEvent_SpellcastStop: Internal utility: HealBot_OnEvent_SpellcastStop -- HealBot_OnEvent_SpellcastStop: Internal utility: HealBot_OnEvent_SpellcastStop
function HealBot_OnEvent_SpellcastStop(this, eventName) function HealBot_OnEvent_SpellcastStop(this, eventName)
HealBot_IsCasting = false; HealBot_IsCasting = false;
HealBot_PendingShapeshiftCast = nil;
if eventName == "SPELLCAST_FAILED" then if eventName == "SPELLCAST_FAILED" then
HealBot_CastFailed = true; HealBot_CastFailed = true;
end end
+23 -9
View File
@@ -171,6 +171,19 @@ end
-- HealBot_StartCasting: Initiates spell cast and broadcasts incoming heal. -- HealBot_StartCasting: Initiates spell cast and broadcasts incoming heal.
function HealBot_StartCasting(spell, target, ttype) function HealBot_StartCasting(spell, target, ttype)
HealBot_CastFailed = false; HealBot_CastFailed = false;
-- Extract base spell for internal tracking
local baseSpell = spell
local parenIndex = string.find(spell, " %(")
if parenIndex then
baseSpell = string.sub(spell, 1, parenIndex - 1)
else
parenIndex = string.find(spell, "%(")
if parenIndex then
baseSpell = string.sub(spell, 1, parenIndex - 1)
end
end
HealBot_CastSpellByName(spell); HealBot_CastSpellByName(spell);
if ( SpellCanTargetUnit(target) ) then if ( SpellCanTargetUnit(target) ) then
SpellTargetUnit(target); SpellTargetUnit(target);
@@ -184,14 +197,16 @@ function HealBot_StartCasting(spell, target, ttype)
end end
end end
if ttype == "fired" and HealBot_Spells[spell] then if ttype == "fired" and HealBot_Spells[baseSpell] then
if not HealBot_CastFailed then if not HealBot_CastFailed then
HealBot_CastingSpell = spell; HealBot_CastingSpell = baseSpell;
HealBot_CastingTarget = target; HealBot_CastingTarget = target;
HealBot_Process_HealValue(spell, target); HealBot_Process_HealValue(baseSpell, target);
HealBot_AnnounceCast(spell, target); HealBot_AnnounceCast(spell, target);
end end
end end
return ttype == "fired"
end end
-- HealBot_StopCasting: Internal utility: HealBot_StopCasting -- HealBot_StopCasting: Internal utility: HealBot_StopCasting
@@ -308,7 +323,9 @@ function HealBot_CastSpellOnFriend(spell, target)
end end
if formCancelled then if formCancelled then
HealBot_PendingShapeshiftCast = { spell = spell, target = target, targetEnemy = targetEnemy, oldTarget = oldTarget, expires = GetTime() + 1.0 } -- ALWAYS put the cast into the Pending queue so the OnUpdate loop can wait for the server
-- to process the unshift before attempting the cast, otherwise we get "You are in shapeshift form".
HealBot_PendingShapeshiftCast = { spell = spell, target = target, targetEnemy = targetEnemy, oldTarget = oldTarget, fireTime = GetTime() + 0.05, expires = GetTime() + 1.0 }
return; return;
end end
@@ -839,9 +856,9 @@ function HealBot_Generic_Patten(matchStr, matchPattern)
return tmpTest, _HealsMin, _HealsMax; return tmpTest, _HealsMin, _HealsMax;
end end
-- HealBot_UpdateShapeshiftForm: Internal utility: HealBot_UpdateShapeshiftForm -- HealBot_UpdateShapeshiftForm: Called on UPDATE_SHAPESHIFT_FORM
function HealBot_UpdateShapeshiftForm() function HealBot_UpdateShapeshiftForm()
-- Deprecated, state is pulled in real-time on cast -- Deprecated
end end
-- HealBot_GetShapeshiftForm: Detects active druid form to prevent invalid casts. -- HealBot_GetShapeshiftForm: Detects active druid form to prevent invalid casts.
@@ -852,12 +869,9 @@ function HealBot_GetShapeshiftForm()
for i=1,forms do for i=1,forms do
local icon,name,active = GetShapeshiftFormInfo(i); local icon,name,active = GetShapeshiftFormInfo(i);
if active then if active then
local icon_lower = string.lower(icon);
if not string.find(icon_lower, "humanoidform") and not string.find(icon_lower, "treeoflife") and not string.find(icon_lower, "stoneclawtotem") then
return i; return i;
end end
end end
end end
end
return nil; return nil;
end end
+2 -1
View File
@@ -27,8 +27,9 @@ HealBot_ConfigDefaults = {
ActionVisible = 1, ActionVisible = 1,
HideSolo = 0, HideSolo = 0,
OverHeal = 0.25, OverHeal = 0.25,
CastNotify = 1, UpdateFreq = 0.5,
AutoUnshift = 1, AutoUnshift = 1,
Integrations_Nampower_Active = true,
ChatMessages = { ChatMessages = {
[1] = { Spell = "None", Message = "Casting #Spell# on #Target#", Channel = "None" }, [1] = { Spell = "None", Message = "Casting #Spell# on #Target#", Channel = "None" },
[2] = { Spell = "None", Message = "Casting #Spell# on #Target#", Channel = "None" }, [2] = { Spell = "None", Message = "Casting #Spell# on #Target#", Channel = "None" },
+18 -3
View File
@@ -33,7 +33,7 @@
</Anchors> </Anchors>
<Scripts> <Scripts>
<OnLoad> <OnLoad>
getglobal(this:GetName().."Text"):SetText("Enable nampower Integration (Buffs/Heals)"); getglobal(this:GetName().."Text"):SetText("Enable nampower Integration (Buffs/Heals/Queueing)");
</OnLoad> </OnLoad>
<OnClick> <OnClick>
HealBot_Config.HealBot_Integrations_Nampower = this:GetChecked() and 1 or 0; HealBot_Config.HealBot_Integrations_Nampower = this:GetChecked() and 1 or 0;
@@ -44,9 +44,9 @@
<CheckButton name="HealBot_Options_Integrations_SuperWoW" inherits="OptionsCheckButtonTemplate"> <CheckButton name="HealBot_Options_Integrations_SuperWoW" inherits="OptionsCheckButtonTemplate">
<Anchors> <Anchors>
<Anchor point="TOPLEFT" relativeTo="HealBot_Options_Integrations_Nampower" relativePoint="BOTTOMLEFT"> <Anchor point="TOPLEFT">
<Offset> <Offset>
<AbsDimension x="0" y="5"/> <AbsDimension x="31" y="-145"/>
</Offset> </Offset>
</Anchor> </Anchor>
</Anchors> </Anchors>
@@ -81,6 +81,21 @@
</CheckButton> </CheckButton>
</Frames> </Frames>
<Layers>
<Layer level="ARTWORK">
<FontString name="HealBot_Options_Integrations_NampowerInfo" inherits="GameFontHighlightSmall" justifyH="LEFT" justifyV="TOP" text="Note: For the 1-click Shapeshift auto-unshift to work smoothly, you must DISABLE 'Retry server rejected spells' in Nampower ENABLE Queue Instant Cast Spells and Queue Cast Time Spells.">
<Size x="330" y="30"/>
<Anchors>
<Anchor point="TOPLEFT">
<Offset>
<AbsDimension x="65" y="-95"/>
</Offset>
</Anchor>
</Anchors>
<Color r="0.7" g="0.7" b="0.7"/>
</FontString>
</Layer>
</Layers>
<Scripts> <Scripts>
<OnShow> <OnShow>
HealBot_Options_Integrations_OnShow(this); HealBot_Options_Integrations_OnShow(this);
+2
View File
@@ -59,6 +59,8 @@ Default installation path: `C:\Program Files\World of Warcraft\Interface\AddOns\
* **Bug Fix - Slow Client Init** - Fixed missing class colors after `/reload` by adding lazy-load identity fetches directly into the render pipeline for when `UnitClass` data is delayed by the server. * **Bug Fix - Slow Client Init** - Fixed missing class colors after `/reload` by adding lazy-load identity fetches directly into the render pipeline for when `UnitClass` data is delayed by the server.
* **Bug Fix - Debuff Tracking** - HealBot now automatically tracks all curable debuffs for the player's class out of the box. Previously, users had to manually assign a cure spell to a click binding to enable CDC debuff tracking. * **Bug Fix - Debuff Tracking** - HealBot now automatically tracks all curable debuffs for the player's class out of the box. Previously, users had to manually assign a cure spell to a click binding to enable CDC debuff tracking.
* **Bug Fix - CDC Filter** - Fixed an issue where the CDC module displayed all debuffs instead of filtering for dispellable ones, restoring proper health bar coloring. * **Bug Fix - CDC Filter** - Fixed an issue where the CDC module displayed all debuffs instead of filtering for dispellable ones, restoring proper health bar coloring.
* **Bug Fix - UnitClass Localization** - Added a fallback mapping for the Vanilla `UnitClass` API to always return the uppercase English class string. This fixes a bug where default options (like Debuff tracking) failed to initialize.
* **Feature - Shapeshift Spell Queue** - Built a native spell queue system for Druid auto-unshifting. Bypasses the "Cannot cast while shapeshifted" error and server lag when AutoUnshift is enabled, firing the heal seamlessly the exact millisecond the form is dropped. Now completely unified to use the standard casting API for maximum reliability across all client setups.
**v1.6.5** **v1.6.5**
* **Performance Update - Mana Tracking** - Added a fast-path redraw pipeline for unit power changes. This optimization prevents full frame redraws when players naturally regenerate or consume mana, resolving severe FPS drops in 40-man raids while tracking mana. * **Performance Update - Mana Tracking** - Added a fast-path redraw pipeline for unit power changes. This optimization prevents full frame redraws when players naturally regenerate or consume mana, resolving severe FPS drops in 40-man raids while tracking mana.