diff --git a/engine/class_modules/warlock/sc_warlock.cpp b/engine/class_modules/warlock/sc_warlock.cpp index f7c0d7255a4..75bbbe64331 100644 --- a/engine/class_modules/warlock/sc_warlock.cpp +++ b/engine/class_modules/warlock/sc_warlock.cpp @@ -96,11 +96,26 @@ warlock_td_t::warlock_td_t( player_t* target, warlock_t& p ) ->set_duration( 0_ms ) ->set_tick_zero( false ) ->set_period( p.hero.blackened_soul_trigger->effectN( 1 ).period() ) - ->set_tick_callback( [ this, target ]( buff_t*, int, timespan_t ) { - warlock.proc_actions.blackened_soul->execute_on_target( target ); + ->set_tick_callback( [ &p, target ]( buff_t* blackened_soul_debuff, int, timespan_t ) { + auto tdata = p.get_target_data( target ); + if ( tdata->dots.wither->is_ticking() ) + { + p.proc_actions.blackened_soul->execute_on_target( target ); + } + else + { + // blackened_soul is a 0-duration frozen-stack debuff, so expiring it from its + // tick callback is safe; tick_t will not apply post-callback stack changes. + assert( blackened_soul_debuff->freeze_stacks ); + assert( blackened_soul_debuff->buff_duration() == 0_ms ); + assert( blackened_soul_debuff->expiration.empty() ); + assert( blackened_soul_debuff->tick_event == nullptr ); + blackened_soul_debuff->expire(); + } } ) ->set_tick_behavior( buff_tick_behavior::REFRESH ) - ->set_freeze_stacks( true ); + ->set_freeze_stacks( true ) + ->set_max_stack( 99 ); debuffs.wither = make_buff( *this, "wither", p.hero.wither_dot ) ->set_refresh_behavior( buff_refresh_behavior::DURATION ) @@ -225,7 +240,6 @@ warlock_t::warlock_t( sim_t* sim, util::string_view name, race_e r ) cooldowns.felstorm_icd = get_cooldown( "felstorm_icd" ); cooldowns.echo_of_sargeras = get_cooldown( "echo_of_sargeras_icd" ); cooldowns.blackened_soul = get_cooldown( "blackened_soul_icd" ); - cooldowns.seeds_of_their_demise = get_cooldown( "seeds_of_their_demise_icd" ); resource_regeneration = regen_type::DYNAMIC; regen_caches[ CACHE_HASTE ] = true; diff --git a/engine/class_modules/warlock/sc_warlock.hpp b/engine/class_modules/warlock/sc_warlock.hpp index e5a9dbe2860..84fbe5bfd7a 100644 --- a/engine/class_modules/warlock/sc_warlock.hpp +++ b/engine/class_modules/warlock/sc_warlock.hpp @@ -165,7 +165,7 @@ struct fixed_cycle_proc_t : public proc_rng_t fixed_cycle_proc_t( std::string_view n, player_t* p, unsigned trigger_count_, bool random_initial_state_ = false, proc_reset_counter_fn proc_reset_fn_ = nullptr ) : proc_rng_t( rng_type, n, p ), - proc_reset_fn( proc_reset_fn_ ), + proc_reset_fn( std::move( proc_reset_fn_ ) ), counter( 0u ), trigger_count( trigger_count_ ), random_initial_state( random_initial_state_ ) @@ -817,7 +817,6 @@ struct warlock_t : public parse_player_effects_t propagate_const felstorm_icd; propagate_const echo_of_sargeras; // ICD for Embers of Nihilam rank 4 procs propagate_const blackened_soul; // Internal cooldown on triggering stack increase to Wither - propagate_const seeds_of_their_demise; // Estimated internal cooldown, a guess at how Blizzard is minimizing lucky streaks } cooldowns; // Buffs @@ -994,6 +993,7 @@ struct warlock_t : public parse_player_effects_t { threshold_rng_t* agony_energize; threshold_rng_t* nightfall; + threshold_rng_t* seeds_of_their_demise; } progress_rng; struct prd_rng_t @@ -1011,6 +1011,8 @@ struct warlock_t : public parse_player_effects_t accumulated_rng_t* feast_of_souls; accumulated_rng_t* demoniac_imp_fade; accumulated_rng_t* spiteful_reconstitution; + accumulated_rng_t* bleakheart_tactics; + accumulated_rng_t* mark_of_perotharn; double infernal_rapidity_prd_c_value; } prd_rng; @@ -1023,13 +1025,9 @@ struct warlock_t : public parse_player_effects_t simple_proc_t* demonfire_infusion_inc; // TODO: Need to check the type of rng simple_proc_t* alythesss_ire_shift; simple_proc_t* wither_crit_energize; // TODO: Need to check the type of rng - simple_proc_t* blackened_soul; // TODO: Need to check the type of rng and chance value - simple_proc_t* bleakheart_tactics; // TODO: Need to check the type of rng and chance value - simple_proc_t* seeds_of_their_demise; // TODO: Need to check the type of rng and chance value - simple_proc_t* mark_of_perotharn; // TODO: Need to check the type of rng and chance value + simple_proc_t* blackened_soul; } flat_rng; - // TODO: Need to check that these RNG values ​​are still correct in Midnight struct rng_settings_t { struct rng_setting_t @@ -1037,44 +1035,47 @@ struct warlock_t : public parse_player_effects_t double setting_value; double default_value; std::string option_name; + double min = std::numeric_limits::lowest(); + double max = std::numeric_limits::max(); }; // Affliction - rng_setting_t agony_energize = { 0.370, 0.370, "agony_energize" }; - rng_setting_t nightfall = { 0.130, 0.130, "nightfall" }; - rng_setting_t cunning_cruelty_sb = { 0.50, 0.50, "cunning_cruelty_sb" }; - rng_setting_t cunning_cruelty_ds = { 0.25, 0.25, "cunning_cruelty_ds" }; + rng_setting_t agony_energize = { 0.370, 0.370, "agony_energize", 0.0 }; + rng_setting_t nightfall = { 0.130, 0.130, "nightfall", 0.0 }; + rng_setting_t cunning_cruelty_sb = { 0.50, 0.50, "cunning_cruelty_sb", 0.0 }; + rng_setting_t cunning_cruelty_ds = { 0.25, 0.25, "cunning_cruelty_ds", 0.0 }; // Demonology - rng_setting_t demoniac_imp_fade_hard_cap = { 21.0, 21.0, "demoniac_imp_fade_hard_cap" }; - rng_setting_t spiteful_reconstitution = { 0.10, 0.10, "spiteful_reconstitution" }; - rng_setting_t spiteful_reconstitution_hard_cap = { 21.0, 21.0, "spiteful_reconstitution_hard_cap" }; - rng_setting_t demonic_knowledge_rank1_cards = { 6.0, 6.0, "demonic_knowledge_rank1_cards" }; - rng_setting_t demonic_knowledge_rank2_cards = { 12.0, 12.0, "demonic_knowledge_rank2_cards" }; - rng_setting_t demonic_knowledge_deck_size = { 80.0, 80.0, "demonic_knowledge_deck_size" }; + rng_setting_t demoniac_imp_fade_hard_cap = { 21.0, 21.0, "demoniac_imp_fade_hard_cap", 0.0 }; + rng_setting_t spiteful_reconstitution = { 0.10, 0.10, "spiteful_reconstitution", 0.0 }; + rng_setting_t spiteful_reconstitution_hard_cap = { 21.0, 21.0, "spiteful_reconstitution_hard_cap", 0.0 }; + rng_setting_t demonic_knowledge_rank1_cards = { 6.0, 6.0, "demonic_knowledge_rank1_cards", 0.0 }; + rng_setting_t demonic_knowledge_rank2_cards = { 12.0, 12.0, "demonic_knowledge_rank2_cards", 0.0 }; + rng_setting_t demonic_knowledge_deck_size = { 80.0, 80.0, "demonic_knowledge_deck_size", 0.0 }; // Destruction - rng_setting_t rain_of_chaos_cards = { 3.0, 3.0, "rain_of_chaos_cards" }; - rng_setting_t rain_of_chaos_deck_size = { 20.0, 20.0, "rain_of_chaos_deck_size" }; - rng_setting_t alythesss_ire_shift = { 0.01, 0.01, "alythesss_ire_shift" }; - rng_setting_t echo_of_sargeras = { 0.10, 0.10, "echo_of_sargeras" }; + rng_setting_t rain_of_chaos_cards = { 3.0, 3.0, "rain_of_chaos_cards", 0.0 }; + rng_setting_t rain_of_chaos_deck_size = { 20.0, 20.0, "rain_of_chaos_deck_size", 0.0 }; + rng_setting_t alythesss_ire_shift = { 0.01, 0.01, "alythesss_ire_shift", 0.0 }; + rng_setting_t echo_of_sargeras = { 0.10, 0.10, "echo_of_sargeras", 0.0 }; // Diabolist // Hellcaller - rng_setting_t blackened_soul = { 0.10, 0.10, "blackened_soul" }; - rng_setting_t bleakheart_tactics = { 0.15, 0.15, "bleakheart_tactics" }; - rng_setting_t seeds_of_their_demise = { 0.15, 0.15, "seeds_of_their_demise" }; - rng_setting_t mark_of_perotharn = { 0.15, 0.15, "mark_of_perotharn" }; + rng_setting_t blackened_soul = { 0.23, 0.23, "blackened_soul", 0.0 }; + rng_setting_t bleakheart_tactics = { 0.15, 0.15, "bleakheart_tactics", 0.0 }; + rng_setting_t seeds_of_their_demise = { 0.240, 0.240, "seeds_of_their_demise", 0.0 }; + rng_setting_t mark_of_perotharn = { 0.15, 0.15, "mark_of_perotharn", 0.0 }; // Soul Harvester - rng_setting_t succulent_soul_aff = { 0.225, 0.225, "succulent_soul_aff" }; - rng_setting_t succulent_soul_demo = { 0.15, 0.15, "succulent_soul_demo" }; - rng_setting_t feast_of_souls_aff = { 0.04, 0.04, "feast_of_souls_aff" }; - rng_setting_t feast_of_souls_demo = { 0.10, 0.10, "feast_of_souls_demo" }; - rng_setting_t feast_of_souls_hard_cap_aff = { 26.0, 26.0, "feast_of_souls_hard_cap_aff" }; - rng_setting_t feast_of_souls_hard_cap_demo = { 26.0, 26.0, "feast_of_souls_hard_cap_demo" }; - rng_setting_t manifested_avarice = { 0.10, 0.10, "manifested_avarice" }; + rng_setting_t succulent_soul_aff = { 0.225, 0.225, "succulent_soul_aff", 0.0 }; + rng_setting_t succulent_soul_demo = { 0.15, 0.15, "succulent_soul_demo", 0.0 }; + rng_setting_t feast_of_souls_aff = { 0.12, 0.12, "feast_of_souls_aff", 0.0 }; + rng_setting_t feast_of_souls_aff_quietus = { 0.04, 0.04, "feast_of_souls_aff_quietus", 0.0 }; + rng_setting_t feast_of_souls_demo = { 0.10, 0.10, "feast_of_souls_demo", 0.0 }; + rng_setting_t feast_of_souls_hard_cap_aff = { 26.0, 26.0, "feast_of_souls_hard_cap_aff", 0.0 }; + rng_setting_t feast_of_souls_hard_cap_demo = { 26.0, 26.0, "feast_of_souls_hard_cap_demo", 0.0 }; + rng_setting_t manifested_avarice = { 0.10, 0.10, "manifested_avarice", 0.0 }; template void for_each( F&& f ) @@ -1100,6 +1101,7 @@ struct warlock_t : public parse_player_effects_t f( succulent_soul_aff ); f( succulent_soul_demo ); f( feast_of_souls_aff ); + f( feast_of_souls_aff_quietus ); f( feast_of_souls_demo ); f( feast_of_souls_hard_cap_aff ); f( feast_of_souls_hard_cap_demo ); diff --git a/engine/class_modules/warlock/sc_warlock_actions.cpp b/engine/class_modules/warlock/sc_warlock_actions.cpp index 34eff9e630d..944131f6647 100644 --- a/engine/class_modules/warlock/sc_warlock_actions.cpp +++ b/engine/class_modules/warlock/sc_warlock_actions.cpp @@ -868,7 +868,7 @@ using namespace helpers; if ( p()->hero.quietus.ok() && p()->hero.shared_fate.ok() ) p()->proc_actions.shared_fate->execute_on_target( target ); - if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger() ) + if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger( execute_state ) ) p()->feast_of_souls_gain(); } @@ -1120,20 +1120,30 @@ using namespace helpers; } // Seeds of their Demise collapse conditions must be checked periodically for every Wither tick - if ( !td( d->target )->debuffs.blackened_soul->check() ) + bool collapse = false; + collapse = collapse || ( p()->hero.seeds_of_their_demise.ok() && d->current_stack() > 1 && d->target->health_percentage() <= p()->hero.seeds_of_their_demise->effectN( 2 ).base_value() ); + collapse = collapse || ( p()->hero.seeds_of_their_demise.ok() && d->current_stack() >= as( p()->hero.seeds_of_their_demise->effectN( 1 ).base_value() ) ); + if ( collapse ) { - bool collapse = false; - collapse = collapse || ( p()->hero.seeds_of_their_demise.ok() && d->current_stack() > 1 && d->target->health_percentage() <= p()->hero.seeds_of_their_demise->effectN( 2 ).base_value() ); - collapse = collapse || ( p()->hero.seeds_of_their_demise.ok() && d->current_stack() >= as( p()->hero.seeds_of_their_demise->effectN( 1 ).base_value() ) ); - if ( collapse ) + const int prev_collapse_stacks = td( d->target )->debuffs.blackened_soul->check(); + assert( prev_collapse_stacks >= 0 ); + const int diff_stacks = d->current_stack() - prev_collapse_stacks; + + assert( d->current_stack() >= 1 ); + if ( diff_stacks > 0 ) + td( d->target )->debuffs.blackened_soul->trigger( diff_stacks ); + else if ( diff_stacks < 0 ) + td( d->target )->debuffs.blackened_soul->decrement( -diff_stacks ); + + assert( td( d->target )->debuffs.blackened_soul->check() ); + if ( !prev_collapse_stacks ) { - td( d->target )->debuffs.blackened_soul->trigger(); p()->sim->print_debug( "{} wither stack collapse in {} started (seeds of their demise) (wither tick check). wither_current_stack={}, wither_target_health_percentage={:.2f}%", p()->name(), d->target->name(), d->current_stack(), d->target->health_percentage() ); } } - if ( d->state->result == RESULT_CRIT && p()->hero.mark_of_perotharn.ok() && p()->flat_rng.mark_of_perotharn->trigger() ) + if ( d->state->result == RESULT_CRIT && p()->hero.mark_of_perotharn.ok() && p()->prd_rng.mark_of_perotharn->trigger() ) { // Wither stack gain by Mark of Perotharn does not directly trigger collapse in that tick (it will be trigged on the next tick) // Wither stack gain by Mark of Perotharn does not benefit from Bleakheart Tactics @@ -1200,7 +1210,7 @@ using namespace helpers; { warlock_spell_t::impact( s ); - if ( s->result == RESULT_CRIT && p()->hero.mark_of_perotharn.ok() && p()->flat_rng.mark_of_perotharn->trigger() ) + if ( s->result == RESULT_CRIT && p()->hero.mark_of_perotharn.ok() && p()->prd_rng.mark_of_perotharn->trigger() ) { auto& wither_dot = td( s->target )->dots.wither; auto& wither_debuff = td( s->target )->debuffs.wither; @@ -1260,35 +1270,37 @@ using namespace helpers; player_t* tar = s->target; + // Blackened Soul damage impact runs during blackened_soul_debuff tick callback. + // Its frozen stacks make direct expire/decrement safe here without deferring to a follow-up event. + auto& blackened_soul_debuff = td( tar )->debuffs.blackened_soul; + assert( blackened_soul_debuff->check() ); + assert( blackened_soul_debuff->freeze_stacks ); + assert( blackened_soul_debuff->buff_duration() == 0_ms ); + assert( blackened_soul_debuff->expiration.empty() ); + assert( blackened_soul_debuff->tick_event == nullptr ); if ( td( tar )->dots.wither->current_stack() <= 1 ) { - make_event( *sim, 0_ms, [ this, tar ] { - if ( td( tar )->debuffs.blackened_soul->check() ) - { - td( tar )->debuffs.blackened_soul->expire(); - p()->sim->print_debug( "{} wither stack collapse in {} ended. wither_current_stack={}", p()->name(), tar->name(), td( tar )->dots.wither->current_stack() ); - } - } ); + blackened_soul_debuff->expire(); + p()->sim->print_debug( "{} wither stack collapse in {} ended (wither stacks reach 1). wither_current_stack={}", p()->name(), tar->name(), td( tar )->dots.wither->current_stack() ); + } + else + { + blackened_soul_debuff->decrement(); + if ( !blackened_soul_debuff->check() ) + p()->sim->print_debug( "{} wither stack collapse in {} ended (collapse consumed its stacks). wither_current_stack={}", p()->name(), tar->name(), td( tar )->dots.wither->current_stack() ); } - bool seeds_triggered = false; - - if ( affliction() && p()->hero.seeds_of_their_demise.ok() && p()->cooldowns.seeds_of_their_demise->up() && p()->flat_rng.seeds_of_their_demise->trigger() ) + if ( affliction() && p()->hero.seeds_of_their_demise.ok() && p()->progress_rng.seeds_of_their_demise->trigger( s ) ) { p()->buffs.shard_instability->trigger(); p()->procs.seeds_of_their_demise->occur(); - seeds_triggered = true; } - if ( destruction() && p()->hero.seeds_of_their_demise.ok() && p()->cooldowns.seeds_of_their_demise->up() && p()->flat_rng.seeds_of_their_demise->trigger() ) + if ( destruction() && p()->hero.seeds_of_their_demise.ok() && p()->progress_rng.seeds_of_their_demise->trigger( s ) ) { - p()->buffs.flashpoint->trigger( 2 ); + p()->buffs.flashpoint->trigger( as( p()->hero.seeds_of_their_demise->effectN( 3 ).base_value() ) ); p()->procs.seeds_of_their_demise->occur(); - seeds_triggered = true; } - - if ( seeds_triggered ) - p()->cooldowns.seeds_of_their_demise->start(); } }; @@ -1540,7 +1552,6 @@ using namespace helpers; if ( p()->talents.cascading_calamity.ok() && dot->is_ticking() ) p()->buffs.cascading_calamity->trigger(); - // timespan_t dot_new_last_duration = dot->time_to_next_full_tick() + composite_dot_duration( s ); // TODO: Alternative that takes into account the extra tick on refresh; which is more appropriate? timespan_t dot_new_last_duration = composite_dot_duration( s ); // NOTE: If Blizzard change the UA DoT Behavior, this need to be redesigned assert( dot_behavior == DOT_REFRESH_DURATION && "UA DoT Behavior has changed" ); @@ -1871,7 +1882,7 @@ using namespace helpers; p()->proc_actions.shared_fate->execute_on_target( main_seed_target ); // Feast of Souls is processed before the decrement of Succulent Soul, causing the same SoC cast that gains the Succulent Soul stack to consume it - if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger() ) + if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger( execute_state ) ) p()->feast_of_souls_gain( true ); } @@ -2147,7 +2158,7 @@ using namespace helpers; if ( p()->hero.quietus.ok() && p()->hero.shared_fate.ok() ) p()->proc_actions.shared_fate->execute_on_target( target ); - if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger() ) + if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger( execute_state ) ) p()->feast_of_souls_gain(); } p()->buffs.nightfall->decrement(); @@ -2286,7 +2297,7 @@ using namespace helpers; if ( p()->hero.quietus.ok() && p()->hero.shared_fate.ok() ) p()->proc_actions.shared_fate->execute_on_target( target ); - if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger() ) + if ( p()->hero.quietus.ok() && p()->hero.feast_of_souls.ok() && p()->prd_rng.feast_of_souls->trigger( execute_state ) ) p()->feast_of_souls_gain(); } p()->buffs.nightfall->decrement(); @@ -5195,48 +5206,56 @@ using namespace helpers; stacks = as( p->hero.malevolence->effectN( 1 ).base_value() ); } - tdata->dots.wither->increment( stacks ); - tdata->debuffs.wither->bump( stacks ); - assert( tdata->dots.wither->current_stack() == tdata->debuffs.wither->check() && tdata->dots.wither->remains() == tdata->debuffs.wither->remains() ); - stack_gained = true; - // Wither extra stack from Malevolence Effect #2 does not benefit from Bleakheart Tactics if ( p->buffs.malevolence->check() && !malevolence ) { - const int inc = as( p->hero.malevolence->effectN( 2 ).base_value() ); - tdata->dots.wither->increment( inc ); - tdata->debuffs.wither->bump( inc ); - assert( tdata->dots.wither->current_stack() == tdata->debuffs.wither->check() && tdata->dots.wither->remains() == tdata->debuffs.wither->remains() ); + stacks += as( p->hero.malevolence->effectN( 2 ).base_value() ); } + // Bleakheart Tactics proc uses a global BLP (PRD-accumulator) // Malevolence stack gains do not benefit from Bleakheart Tactics - if ( p->hero.bleakheart_tactics.ok() && !malevolence && p->flat_rng.bleakheart_tactics->trigger() ) + if ( p->hero.bleakheart_tactics.ok() && !malevolence && p->prd_rng.bleakheart_tactics->trigger() ) { - const int inc = as( p->hero.bleakheart_tactics->effectN( 3 ).base_value() ); - tdata->dots.wither->increment( inc ); - tdata->debuffs.wither->bump( inc ); - assert( tdata->dots.wither->current_stack() == tdata->debuffs.wither->check() && tdata->dots.wither->remains() == tdata->debuffs.wither->remains() ); + stacks += as( p->hero.bleakheart_tactics->effectN( 3 ).base_value() ); p->procs.bleakheart_tactics->occur(); } - if ( !tdata->debuffs.blackened_soul->check() ) + assert( stacks >= 1 ); + tdata->dots.wither->increment( stacks ); + tdata->debuffs.wither->bump( stacks ); + assert( tdata->dots.wither->current_stack() == tdata->debuffs.wither->check() && tdata->dots.wither->remains() == tdata->debuffs.wither->remains() ); + stack_gained = true; + + const int prev_collapse_stacks = tdata->debuffs.blackened_soul->check(); + assert( prev_collapse_stacks >= 0 ); + bool collapse = false; // Malevolence no longer initiates collapse automatically. Last tested 2026-03-17 + collapse = collapse || ( p->hero.seeds_of_their_demise.ok() && tdata->dots.wither->current_stack() > 1 && target->health_percentage() <= p->hero.seeds_of_their_demise->effectN( 2 ).base_value() ); + collapse = collapse || ( p->hero.seeds_of_their_demise.ok() && tdata->dots.wither->current_stack() >= as( p->hero.seeds_of_their_demise->effectN( 1 ).base_value() ) ); + + if ( collapse ) { - bool collapse = false; // Malevolence no longer initiates collapse automatically. Last tested 2026-03-17 - collapse = collapse || ( p->hero.seeds_of_their_demise.ok() && tdata->dots.wither->current_stack() > 1 && target->health_percentage() <= p->hero.seeds_of_their_demise->effectN( 2 ).base_value() ); - collapse = collapse || ( p->hero.seeds_of_their_demise.ok() && tdata->dots.wither->current_stack() >= as( p->hero.seeds_of_their_demise->effectN( 1 ).base_value() ) ); + const int diff_stacks = tdata->dots.wither->current_stack() - prev_collapse_stacks; - if ( collapse ) + assert( tdata->dots.wither->current_stack() >= 1 ); + if ( diff_stacks > 0 ) + tdata->debuffs.blackened_soul->trigger( diff_stacks ); + else if ( diff_stacks < 0 ) + tdata->debuffs.blackened_soul->decrement( -diff_stacks ); + + assert( tdata->debuffs.blackened_soul->check() ); + if ( !prev_collapse_stacks ) { - tdata->debuffs.blackened_soul->trigger(); p->sim->print_debug( "{} wither stack collapse in {} started (seeds of their demise) (stack gain check). wither_current_stack={}, wither_target_health_percentage={:.2f}%", p->name(), target->name(), tdata->dots.wither->current_stack(), target->health_percentage() ); } - else if ( p->flat_rng.blackened_soul->trigger() && !malevolence ) // Malevolence stack gains do not trigger Blackened Soul collapse proc - { - tdata->debuffs.blackened_soul->trigger(); - p->procs.blackened_soul->occur(); - p->sim->print_debug( "{} wither stack collapse in {} started (blackened soul proc). wither_current_stack={}", p->name(), target->name(), tdata->dots.wither->current_stack() ); - } + } + else if ( !prev_collapse_stacks && !malevolence && p->flat_rng.blackened_soul->trigger() ) // Malevolence stack gains do not trigger Blackened Soul collapse proc + { + const int new_collapse_stacks = tdata->dots.wither->current_stack(); + assert( new_collapse_stacks >= 1 && !tdata->debuffs.blackened_soul->check() ); + tdata->debuffs.blackened_soul->trigger( new_collapse_stacks ); + p->procs.blackened_soul->occur(); + p->sim->print_debug( "{} wither stack collapse in {} started (blackened soul proc). wither_current_stack={}", p->name(), target->name(), tdata->dots.wither->current_stack() ); } if ( malevolence ) @@ -5344,8 +5363,6 @@ using namespace helpers; { warlock_t* p = static_cast( player() ); - // if ( dot->is_ticking() && dot->tick_event && dot->current_action && dot->remains() > 0_ms ) // TODO: Alternative that takes into account the extra tick on refresh; which is more appropriate? - // if ( dot->is_ticking() && dot->tick_event && dot->current_action && dot->remains() > 0_ms && dot->current_stack() > 1 ) if ( dot->is_ticking() && dot->tick_event && dot->current_action && dot->remains() > 0_ms ) { player_t* target = dot->target; diff --git a/engine/class_modules/warlock/sc_warlock_init.cpp b/engine/class_modules/warlock/sc_warlock_init.cpp index ac6cd41e6b5..9ce0f600f88 100644 --- a/engine/class_modules/warlock/sc_warlock_init.cpp +++ b/engine/class_modules/warlock/sc_warlock_init.cpp @@ -631,7 +631,6 @@ namespace warlock hero.malevolence_dmg = conditional_spell_lookup( hero.malevolence.ok(), 446285 ); cooldowns.blackened_soul->duration = hero.blackened_soul->internal_cooldown(); - cooldowns.seeds_of_their_demise->duration = 15_s; } void warlock_t::init_spells_soul_harvester() @@ -1180,8 +1179,9 @@ namespace warlock auto tdata = get_target_data( s->target ); assert( tdata ); dot_t* agony_dot = tdata->dots.agony; + assert( agony_dot && agony_dot->is_ticking() ); unsigned active_agonies = get_active_dots( agony_dot ); - assert( agony_dot && agony_dot->is_ticking() && active_agonies > 0 ); + assert( active_agonies > 0 ); increment_max *= std::pow( active_agonies, -2.0 / 3.0 ); return rng().range( 0.0, increment_max ); }, true, true ); @@ -1217,8 +1217,9 @@ namespace warlock auto tdata = get_target_data( s->target ); assert( tdata ); dot_t* corruption_dot = hero.wither.ok() ? tdata->dots.wither : tdata->dots.corruption; + assert( corruption_dot && corruption_dot->is_ticking() ); unsigned active_corruptions = get_active_dots( corruption_dot ); - assert( corruption_dot && corruption_dot->is_ticking() && active_corruptions > 0 ); + assert( active_corruptions > 0 ); increment_max *= std::pow( active_corruptions, -2.0 / 3.0 ); return rng().range( 0.0, increment_max ); }, true, true ); @@ -1260,7 +1261,7 @@ namespace warlock { // Modeling Demoniac (Wild Imp fade) as a pseudo-random distribution (PRD) with a nominal rate of 10% and a hard cap of 21 attempts. // The corresponding PRD constant, calculated with that cap included, is C = 0.014559015812945588. - int demoniac_imp_fade_hardcap = static_cast( rng_settings.demoniac_imp_fade_hard_cap.setting_value ); + unsigned demoniac_imp_fade_hardcap = static_cast( rng_settings.demoniac_imp_fade_hard_cap.setting_value ); double c_dwif = prd::find_constant( talents.demonic_core_spell->effectN( 1 ).percent(), demoniac_imp_fade_hardcap ); prd_rng.demoniac_imp_fade = get_accumulated_rng( "demoniac_imp_fade", c_dwif, demoniac_imp_fade_hardcap ); @@ -1287,7 +1288,7 @@ namespace warlock if ( talents.spiteful_reconstitution.ok() ) { double c_sr = prd::find_constant( rng_settings.spiteful_reconstitution.setting_value ); - int spiteful_reconstitution_hardcap = static_cast( rng_settings.spiteful_reconstitution_hard_cap.setting_value ); + unsigned spiteful_reconstitution_hardcap = static_cast( rng_settings.spiteful_reconstitution_hard_cap.setting_value ); prd_rng.spiteful_reconstitution = get_accumulated_rng( "spiteful_reconstitution", c_sr, spiteful_reconstitution_hardcap ); } @@ -1402,9 +1403,43 @@ namespace warlock { flat_rng.wither_crit_energize = get_simple_proc_rng( "wither_crit_energize", hero.wither_direct->effectN( 2 ).percent() ); flat_rng.blackened_soul = get_simple_proc_rng( "blackened_soul", rng_settings.blackened_soul.setting_value ); - flat_rng.bleakheart_tactics = get_simple_proc_rng( "bleakheart_tactics", rng_settings.bleakheart_tactics.setting_value ); - flat_rng.seeds_of_their_demise = get_simple_proc_rng( "seeds_of_their_demise", rng_settings.seeds_of_their_demise.setting_value ); - flat_rng.mark_of_perotharn = get_simple_proc_rng( "mark_of_perotharn", rng_settings.mark_of_perotharn.setting_value ); + + // Modeling Bleakheart Tactics as a shared pseudo-random distribution (PRD) with a nominal + // rate of 15%, which corresponds to PRD constant C = 0.032220914373087675. + if ( hero.bleakheart_tactics.ok() ) + { + double c_bt = prd::find_constant( rng_settings.bleakheart_tactics.setting_value ); + prd_rng.bleakheart_tactics = get_accumulated_rng( "bleakheart_tactics", c_bt ); + } + + // Seeds of their Demise proc + if ( hero.seeds_of_their_demise.ok() ) + { + double base_inc_max = rng_settings.seeds_of_their_demise.setting_value; + + progress_rng.seeds_of_their_demise = get_threshold_rng( "seeds_of_their_demise", base_inc_max, + [ this ]( double increment_max, action_state_t* s ) { + assert( hero.wither.ok() ); + assert( s ); + auto tdata = get_target_data( s->target ); + assert( tdata ); + dot_t* wither_dot = tdata->dots.wither; + assert( wither_dot && wither_dot->is_ticking() ); + const double stacks_before = wither_dot->current_stack() + 1.0; + unsigned active_withers = get_active_dots( wither_dot ); + assert( active_withers > 0 ); + const double weight = std::pow( stacks_before, -2.0 / 3.0 ) * std::pow( active_withers, -3.0 / 4.0 ); + return rng().range( increment_max * weight ); + }, true, true ); + } + + // Modeling Mark of Perotharn as a shared pseudo-random distribution (PRD) with a nominal + // rate of 15%, which corresponds to PRD constant C = 0.032220914373087675. + if ( hero.mark_of_perotharn.ok() ) + { + double c_mop = prd::find_constant( rng_settings.mark_of_perotharn.setting_value ); + prd_rng.mark_of_perotharn = get_accumulated_rng( "mark_of_perotharn", c_mop ); + } rppm_rng.devil_fruit = get_rppm( "devil_fruit", hero.devil_fruit ); } @@ -1433,26 +1468,34 @@ namespace warlock prd_rng.manifested_avarice = get_accumulated_rng( "manifested_avarice", c_ma ); } - // Modeling Feast of Souls as a pseudo-random distribution (PRD) with an uncapped nominal rate of 4% (aff) / 10% (demo). Those - // nominal rates correspond to PRD constants C = 0.002448555471647706 (aff) / C = 0.014745844781072676 (demo). A separate hard + // Modeling Feast of Souls (Kill) as a pseudo-random distribution (PRD) with an uncapped nominal rate of 12% (aff) / 10% (demo), which + // corresponds to PRD constants C = 0.020983228162532177 (aff) / C = 0.014745844781072676 (demo). Due to a possible bug, Affliction FoS + // from Quietus shares the same PRD, but with a lower activation chance. + // Modeling Feast of Souls (Quietus) as a pseudo-random distribution (PRD) with an uncapped nominal rate of 4% (aff) / 10% (demo). + // Those nominal rates correspond to PRD constants C = 0.002448555471647706 (aff) / C = 0.014745844781072676 (demo). A separate hard // cap of 26 attempts is then applied on top of the PRD, raising the effective average proc chance to ~4.94% (aff) / ~10.01% (demo). if ( hero.feast_of_souls.ok() ) { assert( affliction() || demonology() ); - double c_fs = 0.0; - int feast_of_souls_hardcap = 0; if ( affliction() ) { - c_fs = prd::find_constant( rng_settings.feast_of_souls_aff.setting_value ); - feast_of_souls_hardcap = static_cast( rng_settings.feast_of_souls_hard_cap_aff.setting_value ); + double c_fs = prd::find_constant( rng_settings.feast_of_souls_aff.setting_value ); + double c_fsq = prd::find_constant( rng_settings.feast_of_souls_aff_quietus.setting_value ); + unsigned feast_of_souls_hardcap = static_cast( rng_settings.feast_of_souls_hard_cap_aff.setting_value ); + prd_rng.feast_of_souls = get_accumulated_rng( "feast_of_souls", c_fs, feast_of_souls_hardcap, + !bugs ? accumulated_rng_fn{} : + [ c_fsq, cap = feast_of_souls_hardcap ]( double c_fs, unsigned trigger_count, action_state_t* s ) -> double + { + return ( cap > 0 && trigger_count >= cap ) ? 1.0 : ( s ? c_fsq : c_fs ) * trigger_count; + } + ); } else if ( demonology() ) { - c_fs = prd::find_constant( rng_settings.feast_of_souls_demo.setting_value ); - feast_of_souls_hardcap = static_cast( rng_settings.feast_of_souls_hard_cap_demo.setting_value ); + double c_fs = prd::find_constant( rng_settings.feast_of_souls_demo.setting_value ); + unsigned feast_of_souls_hardcap = static_cast( rng_settings.feast_of_souls_hard_cap_demo.setting_value ); + prd_rng.feast_of_souls = get_accumulated_rng( "feast_of_souls", c_fs, feast_of_souls_hardcap ); } - - prd_rng.feast_of_souls = get_accumulated_rng( "feast_of_souls", c_fs, feast_of_souls_hardcap ); } } @@ -1608,7 +1651,11 @@ namespace warlock void warlock_t::add_rng_option( warlock_t::rng_settings_t::rng_setting_t& setting ) { - add_option( opt_float( "warlock.rng_" + setting.option_name, setting.setting_value ) ); + if ( setting.min != std::numeric_limits::lowest() || setting.max != std::numeric_limits::max() ) + add_option( opt_float( "warlock.rng_" + setting.option_name, setting.setting_value, setting.min, setting.max ) ); + else + add_option( opt_float( "warlock.rng_" + setting.option_name, setting.setting_value ) ); + add_option( opt_deprecated( "rng_" + setting.option_name, "warlock.rng_" + setting.option_name ) ); }