Repository: Hekili/hekili
Pull request #4627: New SimC Expression for Hunters (boar_charge)
Author: login: syrifgit
Created: 2025-03-25T16:28:52Z | Merged: 2025-03-27T15:21:49Z

May be referenced in survival APL, Putro just made it and Thyminde is experimenting.


Diff:
diff --git a/Scripts.lua b/Scripts.lua
index cebe0358a..716aaf060 100644
--- a/Scripts.lua
+++ b/Scripts.lua
@@ -538,6 +538,8 @@ do
         { "^!?fiery_brand_dot_primary_ticking$", "fiery_brand_dot_primary_remains" }, -- Vengeance
         { "^!?elemental_equilibrium.active", "elemental_equilibrium.active_remains" }, -- Elemental Shaman
 
+        { "^!?boar_charge.charges_remaining", "boar_charge.remains" }, -- Pack Leader Hunters
+
         { "^!?variable%.([a-z0-9_]+)$", "safenum(variable.%1)"                        },
         { "^!?variable%.([a-z0-9_]+)<=?(.-)$", "0.01+%2-safenum(variable.%1)"         },
         { "^raid_events%.([a-z0-9_]+)%.remains$", "raid_events.%1.remains"            },
diff --git a/TheWarWithin/HunterBeastMastery.lua b/TheWarWithin/HunterBeastMastery.lua
index 87b0e65ac..4811c31fa 100644
--- a/TheWarWithin/HunterBeastMastery.lua
+++ b/TheWarWithin/HunterBeastMastery.lua
@@ -1294,6 +1294,12 @@ spec:RegisterStateExpr( "pack_leader_buff_next_index", function()
     return PackLeaderBuffNextIndex
 end )
 
+local lastBoarSummoned = 0
+
+spec:RegisterStateExpr( "last_boar_summoned", function()
+    return lastBoarSummoned
+end )
+
 local trapUnits = { "target", "focus" }
 local trappableClassifications = {
     rare = true,
@@ -1323,6 +1329,10 @@ spec:RegisterHook( "COMBAT_LOG_EVENT_UNFILTERED", function( _, subtype, _, sourc
                     break
                 end
             end
+        elseif subtype == "SPELL_AURA_REMOVED" and spellID == 472324 then
+            local now = GetTime()
+            -- use lastcast to make sure it wasn't a natural buff disappearing
+            if now - action.kill_command.lastCast <= 1 then lastBoarSummoned = now end
         end
     end
 
@@ -1405,10 +1415,46 @@ spec:RegisterStateTable( "howl_summon", setmetatable( {
     end
 } ) )
 
+-- To support SimC Expressions
+spec:RegisterStateTable( "boar_charge", setmetatable( {
+
+    boar_duration = 6,
+    boar_interval = 3,
+
+    refresh_tracker = setfenv( function()
+        -- reset_precast function
+        last_boar_summoned = nil
+    end, state ),
+
+}, {
+    __index = function( t, k )
+        local elapsed = query_time - last_boar_summoned
+
+        if k == "remains" then
+            return max( 0, boar_charge.boar_duration - elapsed )
+        elseif k == "next_charge"  then
+            if elapsed < 0 or elapsed > boar_charge.boar_duration then
+                return 3600
+            else
+                return ( boar_charge.boar_interval * ( floor( elapsed / boar_charge.boar_interval ) + 1 ) ) - elapsed
+            end
+        elseif k == "charges_remaining" then
+            if elapsed < 0 or elapsed >= boar_charge.boar_duration then
+                return 0
+            else
+                return max( 0, 2 - ( floor( elapsed / boar_charge.boar_interval ) ) )
+            end
+        end
+    end
+} ) )
 
 spec:RegisterHook( "reset_precast", function()
 
-    if talent.howl_of_the_pack_leader.enabled then howl_summon.refresh_cycle() end
+    if talent.howl_of_the_pack_leader.enabled then
+        howl_summon.refresh_cycle()
+        boar_charge.refresh_tracker()
+    end
+
 
     if debuff.tar_trap.up then
         debuff.tar_trap.expires = debuff.tar_trap.applied + 30
diff --git a/TheWarWithin/HunterSurvival.lua b/TheWarWithin/HunterSurvival.lua
index c51d96d5e..3a63137f8 100644
--- a/TheWarWithin/HunterSurvival.lua
+++ b/TheWarWithin/HunterSurvival.lua
@@ -739,6 +739,12 @@ spec:RegisterStateExpr( "pack_leader_buff_next_index", function()
     return PackLeaderBuffNextIndex
 end )
 
+local lastBoarSummoned = 0
+
+spec:RegisterStateExpr( "last_boar_summoned", function()
+    return lastBoarSummoned
+end )
+
 spec:RegisterHook( "runHandler", function( action, pool )
     if buff.camouflage.up and action ~= "camouflage" then removeBuff( "camouflage" ) end
     if buff.feign_death.up and action ~= "feign_death" then removeBuff( "feign_death" ) end
@@ -784,6 +790,10 @@ spec:RegisterCombatLogEvent( function( _, subtype, _,  sourceGUID, sourceName, _
                     break
                 end
             end
+        elseif subtype == "SPELL_AURA_REMOVED" and spellID == 472324 then
+            local now = GetTime()
+            -- use lastcast to make sure it wasn't a natural buff disappearing
+            if now - action.kill_command.lastCast <= 1 then lastBoarSummoned = now end
         end
     end
 end )
@@ -855,9 +865,47 @@ spec:RegisterStateTable( "howl_summon", setmetatable( {
     end
 } ) )
 
+-- To support SimC Expressions
+spec:RegisterStateTable( "boar_charge", setmetatable( {
+
+    boar_duration = 6,
+    boar_interval = 3,
+
+    refresh_tracker = setfenv( function()
+        -- reset_precast function
+        last_boar_summoned = nil
+    end, state ),
+
+}, {
+    __index = function( t, k )
+        local elapsed = query_time - last_boar_summoned
+        local remains = boar_charge.boar_duration - elapsed
+
+        if k == "remains" then
+            return max( 0, remains )
+        elseif k == "next_charge"  then
+            if elapsed < 0 or elapsed > boar_charge.boar_duration then
+                return 3600
+            else
+                return ( boar_charge.boar_interval * ( floor( elapsed / boar_charge.boar_interval ) + 1 ) ) - elapsed
+            end
+        elseif k == "charges_remaining" then
+            if elapsed < 0 or elapsed >= boar_charge.boar_duration then
+                return 0
+            else
+                return max( 0, 2 - ( floor( elapsed / boar_charge.boar_interval ) ) )
+            end
+        end
+    end
+} ) )
+
+
 spec:RegisterHook( "reset_precast", function()
 
-    if talent.howl_of_the_pack_leader.enabled then howl_summon.refresh_cycle() end
+    if talent.howl_of_the_pack_leader.enabled then
+        howl_summon.refresh_cycle()
+        boar_charge.refresh_tracker()
+    end
 
     if buff.coordinated_assault.up and talent.bombardier.enabled then
         state:QueueAuraEvent( "coordinated_assault", TriggerBombardier, buff.coordinated_assault.expires, "AURA_EXPIRATION" )


Reviews:
user: login: Hekili
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-26T03:42:28Z
user: login: Hekili
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-26T03:44:42Z
user: login: Hekili
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-26T03:45:56Z
user: login: syrifgit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-26T03:49:41Z
user: login: syrifgit
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T03:45:52Z

Review comments:
diff_hunk: @@ -1405,10 +1415,47 @@ spec:RegisterStateTable( "howl_summon", setmetatable( {
     end
 } ) )
 
+-- To support SimC Expressions
+spec:RegisterStateTable( "boar_charge", setmetatable( {
+
+    boar_duration = 6,
+    boar_interval = 3,
+
+    refresh_tracker = setfenv( function()
+        -- reset_precast function
+        last_boar_summoned = nil
+    end, state ),
+
+}, {
+    __index = function( t, k )
+        local elapsed = query_time - last_boar_summoned
+        local remains = boar_charge.boar_duration - elapsed
+
+        if k == "remains" then
+            return max( 0, remains )
path: TheWarWithin/HunterBeastMastery.lua
user: login: Hekili
body: If this is the only use of `remains` it can be removed above and this line changed to:

```Lua
return max( 0, boar_charge.boar_duration - elapsed )
```

This doesn't block the PR from being merged.
created_at: 2025-03-26T03:42:28Z
author_association: OWNER
diff_hunk: @@ -1405,10 +1415,47 @@ spec:RegisterStateTable( "howl_summon", setmetatable( {
     end
 } ) )
 
+-- To support SimC Expressions
+spec:RegisterStateTable( "boar_charge", setmetatable( {
+
+    boar_duration = 6,
+    boar_interval = 3,
+
+    refresh_tracker = setfenv( function()
+        -- reset_precast function
+        last_boar_summoned = nil
+    end, state ),
+
+}, {
+    __index = function( t, k )
+        local elapsed = query_time - last_boar_summoned
+        local remains = boar_charge.boar_duration - elapsed
+
+        if k == "remains" then
+            return max( 0, remains )
+        elseif k == "next_charge"  then
path: TheWarWithin/HunterBeastMastery.lua
user: login: Hekili
body: I am debating to myself whether we should implement a fake `boar_charge` action with cooldown/recharge/charges values and then we can `setCharges` or whatever in `reset_precast` and manipulate it via `spendCharges` or `gainChargeTime` or whatever in the relevant handlers.  I think we could set `hidden = true` on the action to prevent it from being selected in the Priority UI.

This also doesn't block the PR from being merged.
created_at: 2025-03-26T03:44:42Z
author_association: OWNER
diff_hunk: @@ -1405,10 +1415,47 @@ spec:RegisterStateTable( "howl_summon", setmetatable( {
     end
 } ) )
 
+-- To support SimC Expressions
+spec:RegisterStateTable( "boar_charge", setmetatable( {
+
+    boar_duration = 6,
+    boar_interval = 3,
+
+    refresh_tracker = setfenv( function()
+        -- reset_precast function
+        last_boar_summoned = nil
+    end, state ),
+
+}, {
+    __index = function( t, k )
+        local elapsed = query_time - last_boar_summoned
+        local remains = boar_charge.boar_duration - elapsed
+
+        if k == "remains" then
+            return max( 0, remains )
+        elseif k == "next_charge"  then
path: TheWarWithin/HunterBeastMastery.lua
user: login: syrifgit
body: It's a purely passive effect (but triggers buffs which the APL might consider in the very near future). SimC coded it as a ground effect with a 3 second tick rate.
created_at: 2025-03-26T03:49:41Z
author_association: COLLABORATOR
diff_hunk: @@ -1405,10 +1415,47 @@ spec:RegisterStateTable( "howl_summon", setmetatable( {
     end
 } ) )
 
+-- To support SimC Expressions
+spec:RegisterStateTable( "boar_charge", setmetatable( {
+
+    boar_duration = 6,
+    boar_interval = 3,
+
+    refresh_tracker = setfenv( function()
+        -- reset_precast function
+        last_boar_summoned = nil
+    end, state ),
+
+}, {
+    __index = function( t, k )
+        local elapsed = query_time - last_boar_summoned
+        local remains = boar_charge.boar_duration - elapsed
+
+        if k == "remains" then
+            return max( 0, remains )
path: TheWarWithin/HunterBeastMastery.lua
user: login: syrifgit
body: I think I used it elsewhere and later removed the other reference to it
created_at: 2025-03-27T03:45:51Z
author_association: COLLABORATOR

PR comments:
user: login: Hekili
created_at: 2025-03-26T03:46:36Z
body: Holding off on merging just so you can reflect on the comments and let me know what you think.  I don't see the APL changes being committed yet.
author_association: OWNER
user: login: syrifgit
created_at: 2025-03-26T05:33:33Z
body: Converted to draft until I can look at it with fresh brain
author_association: COLLABORATOR
user: login: syrifgit
created_at: 2025-03-27T03:47:41Z
body: Updated. I don't think we need to add any manipulation for the boar, just knowing when it will happen is enough for any APL purposes that I've seen in the hunter TC channel
author_association: COLLABORATOR