From 996a33eb398b4dd8074531871eb644c077a1c40b Mon Sep 17 00:00:00 2001 From: Saeldur <5496498+Saeldur@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:37:42 +0000 Subject: [PATCH 1/2] [Devourer] WIP Drain Improvements --- engine/class_modules/sc_demon_hunter.cpp | 77 +++++++++++++----------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/engine/class_modules/sc_demon_hunter.cpp b/engine/class_modules/sc_demon_hunter.cpp index 01eab854db5..ec84ded2946 100644 --- a/engine/class_modules/sc_demon_hunter.cpp +++ b/engine/class_modules/sc_demon_hunter.cpp @@ -1229,8 +1229,6 @@ class demon_hunter_t : public parse_player_effects_t bool shattered_souls_chance_aoe_reduction_linear = false; int entropy_starting_souls = -1; int channel_tick_cutoff_benefit = 2; - double void_metamorphosis_initial_drain = 8.5; - double void_metamorphosis_drain_per_stack = 0.025; } options; demon_hunter_t( sim_t* sim, util::string_view name, race_e r ); @@ -1392,16 +1390,18 @@ class demon_hunter_t : public parse_player_effects_t struct fury_state_t final { timespan_t start_time; - timespan_t last_tick; event_t* next_drain_event; int drain_stacks; demon_hunter_t* actor; - double initial_drain = 10.0; - double drain_per_stack = 0.012; + double initial_drain = 20.0; + double drain_per_stack = 0; + double drain_per_stack_sq = 0.008; + double exp_factor = 0.3; + double exp_power = 0.1; + bool exp_meta = true; fury_state_t( demon_hunter_t* a ) : start_time( timespan_t::min() ), - last_tick( timespan_t::min() ), next_drain_event( nullptr ), drain_stacks( 0 ), actor( a ) @@ -1410,9 +1410,6 @@ class demon_hunter_t : public parse_player_effects_t void init() { - initial_drain = p()->options.void_metamorphosis_initial_drain; - drain_per_stack = p()->options.void_metamorphosis_drain_per_stack; - if ( p()->talent.devourer.soul_glutton.enabled() ) { @@ -1439,20 +1436,17 @@ class demon_hunter_t : public parse_player_effects_t return actor; } - double base_fury_drain_per_second( int stacks ) const - { - return initial_drain + drain_per_stack * stacks; - } + timespan_t time_to_next_tick( int stacks ) const; - timespan_t base_time_to_next_tick( int stacks ) const + double base_fury_drain_per_second( int stacks ) const { - return timespan_t::from_seconds( 1.0 / ( base_fury_drain_per_second( stacks ) ) ); + if ( exp_meta ) + return initial_drain + exp_factor * exp( exp_power * stacks ); + return initial_drain + drain_per_stack * stacks + drain_per_stack_sq * stacks * stacks; } double fury_drain_per_second( int stacks ) const; - timespan_t time_to_next_tick( int stacks ) const; - void drain(); void start(); @@ -9055,12 +9049,25 @@ struct metamorphosis_buff_t : public demon_hunter_buff_t { actions::demon_hunter_energize_t* rolling_torment_energize; - metamorphosis_buff_t( demon_hunter_t* p ) - : base_t( *p, "metamorphosis", p->spec.metamorphosis_buff ), rolling_torment_energize( nullptr ) + metamorphosis_buff_t( demon_hunter_t* dh ) + : base_t( *dh, "metamorphosis", dh->spec.metamorphosis_buff ), rolling_torment_energize( nullptr ) { set_cooldown( timespan_t::zero() ); - buff_period = timespan_t::zero(); - tick_behavior = buff_tick_behavior::NONE; + + if ( dh->specialization() != DEMON_HUNTER_DEVOURER ) + { + disable_ticking( true ); + } + else + { + freeze_stacks = true; + set_tick_callback( [ this ]( buff_t*, int, timespan_t ) { p()->devourer_fury_state.drain_stacks++; } ); + + if ( p()->talent.devourer.soul_glutton.enabled() ) + { + set_period( buff_period * ( 1 + p()->talent.devourer.soul_glutton->effectN( 2 ).percent() ) ); + } + } // Spell 187827 has a Periodic Dummy effect (#7, 2s period) for visual/server logic that // SimC doesn't model. Without this override, init() -> set_period() detects the periodic // effect and sets refresh_behavior=TICK, but tick_behavior=NONE means no tick_event is @@ -9068,12 +9075,11 @@ struct metamorphosis_buff_t : public demon_hunter_buff_t // hardcasting Meta during an active Untethered Rage-procced Meta). set_refresh_behavior( buff_refresh_behavior::DURATION ); - switch ( p->specialization() ) + switch ( dh->specialization() ) { case DEMON_HUNTER_DEVOURER: set_duration( timespan_t::zero() ); set_constant_behavior( buff_constant_behavior::NEVER_CONSTANT ); - disable_ticking( true ); break; case DEMON_HUNTER_HAVOC: demon_hunter_buff_t::set_default_value_from_effect_type( A_HASTE_ALL ); @@ -9088,15 +9094,15 @@ struct metamorphosis_buff_t : public demon_hunter_buff_t break; } - if ( p->talent.demon_hunter.soul_rending->ok() ) + if ( dh->talent.demon_hunter.soul_rending->ok() ) { add_invalidate( CACHE_LEECH ); } - if ( p->talent.devourer.rolling_torment->ok() ) + if ( dh->talent.devourer.rolling_torment->ok() ) { rolling_torment_energize = - p->get_background_action( "rolling_torment_energize" ); + dh->get_background_action( "rolling_torment_energize" ); } } @@ -10277,8 +10283,12 @@ void demon_hunter_t::create_options() add_option( opt_int( "entropy_starting_souls", options.entropy_starting_souls, -1, 50 ) ); add_option( opt_int( "channel_tick_cutoff_benefit", options.channel_tick_cutoff_benefit, 0, 10 ) ); - add_option( opt_float( "void_metamorphosis_initial_drain", options.void_metamorphosis_initial_drain, 0, 100 ) ); - add_option( opt_float( "void_metamorphosis_drain_per_stack", options.void_metamorphosis_drain_per_stack, 0, 100 ) ); + add_option( opt_float( "void_metamorphosis_initial_drain", devourer_fury_state.initial_drain, 0, 100 ) ); + add_option( opt_float( "void_metamorphosis_drain_per_stack", devourer_fury_state.drain_per_stack, 0, 100 ) ); + add_option( opt_float( "void_metamorphosis_drain_per_stack_sq", devourer_fury_state.drain_per_stack_sq, 0, 100 ) ); + add_option( opt_float( "void_metamorphosis_exp_factor", devourer_fury_state.exp_factor, 0, 100 ) ); + add_option( opt_float( "void_metamorphosis_exp_power", devourer_fury_state.exp_power, 0, 100 ) ); + add_option( opt_bool( "void_metamorphosis_exp", devourer_fury_state.exp_meta ) ); } // demon_hunter_t::create_pet =============================================== @@ -12335,7 +12345,6 @@ void demon_hunter_t::fury_state_t::schedule_tick() { assert( !next_drain_event ); - last_tick = actor->sim->current_time(); next_drain_event = make_event( *actor->sim, actor, time_to_next_tick( drain_stacks ) ); } @@ -12353,7 +12362,7 @@ double demon_hunter_t::fury_state_t::fury_drain_per_second( int stacks ) const drain *= 0.15; } - if ( drain_stacks < 6 ) + if ( drain_stacks < 1 ) { // Slow after meta cast drain *= 0.4; @@ -12364,7 +12373,9 @@ double demon_hunter_t::fury_state_t::fury_drain_per_second( int stacks ) const timespan_t demon_hunter_t::fury_state_t::time_to_next_tick( int stacks ) const { - return 1.0_s / fury_drain_per_second( stacks ); + // 2 as it currently drains 2 per event. + // TODO: Don't hardcode this. + return 2.0_s / fury_drain_per_second( stacks ); } void demon_hunter_t::fury_state_t::reschedule_drain() @@ -12390,7 +12401,7 @@ void demon_hunter_t::fury_state_t::reschedule_drain() void demon_hunter_t::fury_state_t::clear_state() { drain_stacks = 0; - start_time = last_tick = timespan_t::min(); + start_time = timespan_t::min(); } void demon_hunter_t::fury_state_t::stop() @@ -12406,8 +12417,6 @@ void demon_hunter_t::fury_state_t::reset() void demon_hunter_t::fury_state_t::drain() { - last_tick = p()->sim->current_time(); - drain_stacks++; p()->active.void_buildup->stats->add_execute( 0_s, p() ); p()->active.void_buildup->consume_resource(); From b5b1d76595247489ac02a327d39d8e3bd09bab86 Mon Sep 17 00:00:00 2001 From: Saeldur <5496498+Saeldur@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:45:14 +0000 Subject: [PATCH 2/2] [Devourer] Update Drain Model --- engine/class_modules/sc_demon_hunter.cpp | 39 +++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/engine/class_modules/sc_demon_hunter.cpp b/engine/class_modules/sc_demon_hunter.cpp index ec84ded2946..f0e79669cf1 100644 --- a/engine/class_modules/sc_demon_hunter.cpp +++ b/engine/class_modules/sc_demon_hunter.cpp @@ -1393,12 +1393,10 @@ class demon_hunter_t : public parse_player_effects_t event_t* next_drain_event; int drain_stacks; demon_hunter_t* actor; - double initial_drain = 20.0; - double drain_per_stack = 0; - double drain_per_stack_sq = 0.008; - double exp_factor = 0.3; - double exp_power = 0.1; - bool exp_meta = true; + double meta_drain_multiplier = 1.0; + double initial_drain = 15.0; + double exp_factor = 1.455; + double exp_power = 0.075; fury_state_t( demon_hunter_t* a ) : start_time( timespan_t::min() ), @@ -1411,11 +1409,20 @@ class demon_hunter_t : public parse_player_effects_t void init() { if ( p()->talent.devourer.soul_glutton.enabled() ) - { - // This is negative and lowers something to 75% for some reason - initial_drain *= 1 - p()->talent.devourer.soul_glutton->effectN( 2 ).percent(); + // Why separate multiplier - Easier to find stuff in the future. + meta_drain_multiplier /= 1 + p()->talent.devourer.soul_glutton->effectN( 2 ).percent(); } + + p()->sim->print_debug( "{} applying overall drain multiplier of {} to initial drain of {}, result: {}", *p(), + meta_drain_multiplier, initial_drain, initial_drain * meta_drain_multiplier ); + + p()->sim->print_debug( "{} applying overall drain multiplier of {} to exp_factor of {}, result: {}", *p(), + meta_drain_multiplier, exp_factor, exp_factor * meta_drain_multiplier ); + + // Handled here to do less runtime math later - If formula changes, may need to move Drain Multiplier into the calculation. + initial_drain *= meta_drain_multiplier; + exp_factor *= meta_drain_multiplier; } void clear_state(); @@ -1440,9 +1447,7 @@ class demon_hunter_t : public parse_player_effects_t double base_fury_drain_per_second( int stacks ) const { - if ( exp_meta ) - return initial_drain + exp_factor * exp( exp_power * stacks ); - return initial_drain + drain_per_stack * stacks + drain_per_stack_sq * stacks * stacks; + return initial_drain + exp_factor * exp( exp_power * stacks ); } double fury_drain_per_second( int stacks ) const; @@ -9062,11 +9067,6 @@ struct metamorphosis_buff_t : public demon_hunter_buff_t { freeze_stacks = true; set_tick_callback( [ this ]( buff_t*, int, timespan_t ) { p()->devourer_fury_state.drain_stacks++; } ); - - if ( p()->talent.devourer.soul_glutton.enabled() ) - { - set_period( buff_period * ( 1 + p()->talent.devourer.soul_glutton->effectN( 2 ).percent() ) ); - } } // Spell 187827 has a Periodic Dummy effect (#7, 2s period) for visual/server logic that // SimC doesn't model. Without this override, init() -> set_period() detects the periodic @@ -10284,11 +10284,8 @@ void demon_hunter_t::create_options() add_option( opt_int( "channel_tick_cutoff_benefit", options.channel_tick_cutoff_benefit, 0, 10 ) ); add_option( opt_float( "void_metamorphosis_initial_drain", devourer_fury_state.initial_drain, 0, 100 ) ); - add_option( opt_float( "void_metamorphosis_drain_per_stack", devourer_fury_state.drain_per_stack, 0, 100 ) ); - add_option( opt_float( "void_metamorphosis_drain_per_stack_sq", devourer_fury_state.drain_per_stack_sq, 0, 100 ) ); add_option( opt_float( "void_metamorphosis_exp_factor", devourer_fury_state.exp_factor, 0, 100 ) ); add_option( opt_float( "void_metamorphosis_exp_power", devourer_fury_state.exp_power, 0, 100 ) ); - add_option( opt_bool( "void_metamorphosis_exp", devourer_fury_state.exp_meta ) ); } // demon_hunter_t::create_pet =============================================== @@ -12365,7 +12362,7 @@ double demon_hunter_t::fury_state_t::fury_drain_per_second( int stacks ) const if ( drain_stacks < 1 ) { // Slow after meta cast - drain *= 0.4; + drain = 15; } return drain;