From 29f4d45f8aeb7db55a8dd4e26fa2941d56259c12 Mon Sep 17 00:00:00 2001
From: Tom Herbert <18316812+taherbert@users.noreply.github.com>
Date: Wed, 11 Mar 2026 18:53:30 -0700
Subject: [PATCH] [DemonHunter] voidfall proc rate is stack-dependent for
vengeance
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
WCL analysis (55,865 eligible casts) shows voidfall building proc
chance decreases with current stack count, rather than being a flat
rate from spell data (effectN(3) = 35%):
0 stacks: 39.8% [95% CI: 39.2-40.4%] (p<0.0001 vs 35%)
1 stack: 32.1% [95% CI: 31.4-32.7%] (p<0.0001 vs 35%)
2 stacks: 27.5% [95% CI: 26.8-28.3%] (p<0.0001 vs 35%)
Vengeance only — Devourer retains the flat spell data rate pending
separate WCL analysis. Defaults exposed as player options for tuning.
Local sim impact on Vengeance Annihilator build: -1.14% DPS
(66,349 -> 65,591), driven by fewer voidfall meteor procs (37.6 ->
36.5 per fight).
---
engine/class_modules/sc_demon_hunter.cpp | 26 +++++++++++++++++++++++-
1 file changed, 25 insertions(+), 1 deletion(-)
diff --git a/engine/class_modules/sc_demon_hunter.cpp b/engine/class_modules/sc_demon_hunter.cpp
index b08d28bd3b5..7c2ff21e248 100644
--- a/engine/class_modules/sc_demon_hunter.cpp
+++ b/engine/class_modules/sc_demon_hunter.cpp
@@ -1215,6 +1215,10 @@ class demon_hunter_t : public parse_player_effects_t
double wounded_quarry_chance_vengeance = 0.30;
// Proc rate for Wounded Quarry for Havoc
double wounded_quarry_chance_havoc = 0.10;
+ // Proc rate for Voidfall per current building stack count (from WCL analysis)
+ double voidfall_proc_chance_0_stacks = 0.40;
+ double voidfall_proc_chance_1_stacks = 0.32;
+ double voidfall_proc_chance_2_stacks = 0.275;
// How many seconds that Vengeful Retreat locks out Felblade
double felblade_lockout_from_vengeful_retreat = 0.6;
bool enable_dungeon_slice = false;
@@ -2935,10 +2939,25 @@ struct voidfall_building_trigger_t : public BASE
{
using base_t = voidfall_building_trigger_t;
+ // Proc chance per current building stack count, cached at construction.
+ // Vengeance uses per-stack rates from WCL analysis; Devourer uses flat spell data rate.
+ std::array voidfall_proc_chances;
+
voidfall_building_trigger_t( util::string_view n, demon_hunter_t* p, const spell_data_t* s = spell_data_t::nil(),
util::string_view o = {} )
: BASE( n, p, s, o )
{
+ if ( p->specialization() == DEMON_HUNTER_VENGEANCE )
+ {
+ voidfall_proc_chances = { p->options.voidfall_proc_chance_0_stacks,
+ p->options.voidfall_proc_chance_1_stacks,
+ p->options.voidfall_proc_chance_2_stacks };
+ }
+ else
+ {
+ double flat = p->talent.annihilator.voidfall->effectN( 3 ).percent();
+ voidfall_proc_chances = { flat, flat, flat };
+ }
}
void execute() override
@@ -2948,7 +2967,9 @@ struct voidfall_building_trigger_t : public BASE
if ( !BASE::p()->talent.annihilator.voidfall->ok() )
return;
- if ( !BASE::rng().roll( BASE::p()->talent.annihilator.voidfall->effectN( 3 ).percent() ) )
+ // clamp to max index; building buff expires at 3 stacks so 2 is the highest we see
+ int stacks = std::min( BASE::p()->buff.voidfall_building->check(), 2 );
+ if ( !BASE::rng().roll( voidfall_proc_chances[ stacks ] ) )
return;
// can't gain building while spending is up
@@ -10113,6 +10134,9 @@ void demon_hunter_t::create_options()
opt_float( "soul_fragment_movement_consume_chance", options.soul_fragment_movement_consume_chance, 0, 1 ) );
add_option( opt_float( "wounded_quarry_chance_vengeance", options.wounded_quarry_chance_vengeance, 0, 1 ) );
add_option( opt_float( "wounded_quarry_chance_havoc", options.wounded_quarry_chance_havoc, 0, 1 ) );
+ add_option( opt_float( "voidfall_proc_chance_0_stacks", options.voidfall_proc_chance_0_stacks, 0, 1 ) );
+ add_option( opt_float( "voidfall_proc_chance_1_stacks", options.voidfall_proc_chance_1_stacks, 0, 1 ) );
+ add_option( opt_float( "voidfall_proc_chance_2_stacks", options.voidfall_proc_chance_2_stacks, 0, 1 ) );
add_option(
opt_float( "felblade_lockout_from_vengeful_retreat", options.felblade_lockout_from_vengeful_retreat, 0, 1 ) );
add_option( opt_bool( "enable_dungeon_slice", options.enable_dungeon_slice ) );