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 ) );