diff --git a/engine/class_modules/warlock/sc_warlock.cpp b/engine/class_modules/warlock/sc_warlock.cpp index 7305cf015fe..e0f97583e1f 100644 --- a/engine/class_modules/warlock/sc_warlock.cpp +++ b/engine/class_modules/warlock/sc_warlock.cpp @@ -233,11 +233,11 @@ warlock_t::warlock_t( sim_t* sim, util::string_view name, race_e r ) regen_caches[ CACHE_SPELL_HASTE ] = true; sim->register_heartbeat_event_callback( [ this ]( sim_t* ) { - // NOTE (2026-03-08): Wild Imps are currently bugged when updating Hellbent Commander stacks on demise. + // NOTE (2026-03-08): Some pets are currently bugged when updating Hellbent Commander stacks on arise/demise. // Hellbent Commander's stacks are updated to their correct value on each heartbeat update. if ( bugs && talents.hellbent_commander.ok() ) { - const int expected_stacks = active_demon_count( !bugs ); + const int expected_stacks = active_demon_count(); const int current_stacks = buffs.hellbent_commander->check(); const int stack_diff = expected_stacks - current_stacks; @@ -484,7 +484,7 @@ int warlock_t::active_demon_count( bool include_diabolist ) const if ( lock_pet->is_sleeping() ) continue; - // NOTE: 2026-02-17 Dibolist guardians seems to not count for some effects/talents (Sacrificed Souls and Hellbent Commander) + // NOTE: 2026-02-17 Dibolist guardians seems to not count for some effects/talents (Sacrificed Souls) if ( !include_diabolist && lock_pet->is_diabolist_guardian ) continue; diff --git a/engine/class_modules/warlock/sc_warlock.hpp b/engine/class_modules/warlock/sc_warlock.hpp index c721d3b6873..56f6279bdca 100644 --- a/engine/class_modules/warlock/sc_warlock.hpp +++ b/engine/class_modules/warlock/sc_warlock.hpp @@ -1053,10 +1053,13 @@ struct warlock_t : public parse_player_effects_t 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 = { 10.0, 10.0, "demonic_knowledge_rank1_cards" }; - rng_setting_t demonic_knowledge_rank2_cards = { 18.0, 18.0, "demonic_knowledge_rank2_cards" }; + 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" }; // 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" }; @@ -1089,6 +1092,9 @@ struct warlock_t : public parse_player_effects_t f( spiteful_reconstitution_hard_cap ); f( demonic_knowledge_rank1_cards ); f( demonic_knowledge_rank2_cards ); + f( demonic_knowledge_deck_size ); + f( rain_of_chaos_cards ); + f( rain_of_chaos_deck_size ); f( alythesss_ire_shift ); f( echo_of_sargeras ); f( blackened_soul ); diff --git a/engine/class_modules/warlock/sc_warlock_init.cpp b/engine/class_modules/warlock/sc_warlock_init.cpp index 742d56ac53f..6cfd2d6fb61 100644 --- a/engine/class_modules/warlock/sc_warlock_init.cpp +++ b/engine/class_modules/warlock/sc_warlock_init.cpp @@ -1304,28 +1304,18 @@ namespace warlock prd_rng.spiteful_reconstitution = get_accumulated_rng( "spiteful_reconstitution", c_sr, spiteful_reconstitution_hardcap ); } - // Demonic Knowledge uses Deck of Cards RNG at 10 out of 80 (rank 1) and 18 out of 80 (rank 2) - // NOTE: 2026-03-06 Demonic Knowledge does not appear to use the average chance indicated in the spell data, but - // rather follows a deck of cards model that also does not match the expected average chance (bug) + // Demonic Knowledge uses Deck of Cards RNG at 6 out of 80 (rank 1) and 12 out of 80 (rank 2) if ( talents.demonic_knowledge.ok() ) { - const int max_cards = 80; - + int deck_size = static_cast( rng_settings.demonic_knowledge_deck_size.setting_value ); int cards = 0; - if ( bugs ) - { - assert( talents.demonic_knowledge.rank() == 2 || talents.demonic_knowledge.rank() == 1 ); - if ( talents.demonic_knowledge.rank() == 2 ) - cards = static_cast( rng_settings.demonic_knowledge_rank2_cards.setting_value ); - else if ( talents.demonic_knowledge.rank() == 1 ) - cards = static_cast( rng_settings.demonic_knowledge_rank1_cards.setting_value ); - } - else - { - cards = static_cast( talents.demonic_knowledge->effectN( 1 ).percent() * max_cards + 0.5 ); - } + assert( talents.demonic_knowledge.rank() == 2 || talents.demonic_knowledge.rank() == 1 ); + if ( talents.demonic_knowledge.rank() == 2 ) + cards = static_cast( rng_settings.demonic_knowledge_rank2_cards.setting_value ); + else if ( talents.demonic_knowledge.rank() == 1 ) + cards = static_cast( rng_settings.demonic_knowledge_rank1_cards.setting_value ); - deck_rng.demonic_knowledge = get_shuffled_rng( "demonic_knowledge", cards, max_cards ); + deck_rng.demonic_knowledge = get_shuffled_rng( "demonic_knowledge", cards, deck_size ); } } @@ -1350,7 +1340,11 @@ namespace warlock } // Rain of Chaos uses Deck of Cards RNG at 3 out of 20 - deck_rng.rain_of_chaos = get_shuffled_rng( "rain_of_chaos", 3, 20 ); + if ( talents.rain_of_chaos.ok() ) { + int deck_size = static_cast( rng_settings.rain_of_chaos_deck_size.setting_value ); + int cards = static_cast( rng_settings.rain_of_chaos_cards.setting_value ); + deck_rng.rain_of_chaos = get_shuffled_rng( "rain_of_chaos", cards, deck_size ); + } // Modeling Dimensional Rift as a pseudo-random distribution (PRD) with a nominal // rate of 10%, which corresponds to PRD constant C = 0.014745844781072676. diff --git a/engine/class_modules/warlock/sc_warlock_pets.cpp b/engine/class_modules/warlock/sc_warlock_pets.cpp index a08bb797436..5e05221e79b 100644 --- a/engine/class_modules/warlock/sc_warlock_pets.cpp +++ b/engine/class_modules/warlock/sc_warlock_pets.cpp @@ -20,7 +20,8 @@ warlock_pet_t::warlock_pet_t( warlock_t* owner, util::string_view pet_name, pet_ owner_coeff.health = 0.5; affected_by.demonic_brutality = owner->talents.demonic_brutality.ok(); - triggers.hellbent_commander = owner->talents.hellbent_commander.ok(); + triggers.hellbent_commander_arise = owner->talents.hellbent_commander.ok(); + triggers.hellbent_commander_demise = owner->talents.hellbent_commander.ok(); register_on_arise_callback( this, [ owner ]() { owner->n_active_pets++; } ); register_on_demise_callback( this, [ owner ]( const player_t* ) { owner->n_active_pets--; } ); @@ -206,10 +207,10 @@ void warlock_pet_t::arise() pet_t::arise(); - if ( triggers.hellbent_commander ) + if ( triggers.hellbent_commander_arise ) { o()->buffs.hellbent_commander->trigger(); - assert( ( bugs || o()->buffs.hellbent_commander->check() == o()->active_demon_count( !bugs ) ) && "Incorrent Demon Count for Hellbent Commander" ); + assert( ( bugs || o()->buffs.hellbent_commander->check() == o()->active_demon_count() ) && "Incorrent Demon Count for Hellbent Commander" ); } } @@ -217,7 +218,7 @@ void warlock_pet_t::demise() { if ( !current.sleeping ) { - if ( triggers.hellbent_commander ) + if ( triggers.hellbent_commander_demise ) { o()->buffs.hellbent_commander->decrement(); } @@ -228,7 +229,7 @@ void warlock_pet_t::demise() if ( melee_attack ) melee_attack->reset(); - assert( ( bugs || !o()->talents.hellbent_commander.ok() || o()->buffs.hellbent_commander->check() == o()->active_demon_count( !bugs ) ) && "Incorrent Demon Count for Hellbent Commander" ); + assert( ( bugs || !o()->talents.hellbent_commander.ok() || o()->buffs.hellbent_commander->check() == o()->active_demon_count() ) && "Incorrent Demon Count for Hellbent Commander" ); } // TODO: Add all pet spells to base warlock data @@ -631,8 +632,8 @@ wild_imp_pet_t::wild_imp_pet_t( warlock_t* owner ) { npc_id = owner->warlock_base.wild_imp->effectN( 1 ).misc_value1(); - // Manually handle the Wild Imps contribution to Hellbent Commander to replicate its bugged behavior - triggers.hellbent_commander &= !bugs; + // Manually handle the Wild Imps contribution to Hellbent Commander on demise to replicate its bugged behavior + triggers.hellbent_commander_demise &= !bugs; resource_regeneration = regen_type::DISABLED; owner_coeff.health = 0.15; @@ -775,10 +776,6 @@ void wild_imp_pet_t::arise() } } - // Manual handling of Hellbent Commander buff for Wild Imps - if ( bugs && o()->talents.hellbent_commander.ok() ) - o()->buffs.hellbent_commander->trigger(); - // Set initial timers for Infernal Command buff sequence of events infernal_command_ev_ts = sim->current_time() + 5045_ms; infernal_command_ev_offset = o()->wild_imp_ic_shared_offset; @@ -833,7 +830,7 @@ void wild_imp_pet_t::demise() } // Manual handling of Hellbent Commander buff for Wild Imps - // NOTE (2026-03-08): Wild Imps are currently bugged when updating Hellbent Commander stacks on demise: + // NOTE (2026-04-08): Wild Imps are currently bugged when updating Hellbent Commander stacks on demise: // If imploded, imps summoned via HoG decrease one stack each, while those summoned via Inner Demons, // Spiteful Reconstitution, or To Hell and Back do not decrease any stacks. // If the imps demise normally or are sacrificed with Power Siphon, HoG imps decrease two stacks each, @@ -1166,6 +1163,9 @@ vilefiend_t::vilefiend_t( warlock_t* owner ) npc_id = owner->talents.vilefiend->effectN( 1 ).misc_value1(); } + // NOTE: 2026-04-08 Vilefiend do not trigger Hellbent Commander on demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_demise &= !bugs; + action_list_str = "bile_spit"; action_list_str += "/travel"; action_list_str += "/headbutt"; @@ -1341,6 +1341,9 @@ demonic_tyrant_t::demonic_tyrant_t( warlock_t* owner, util::string_view name ) { npc_id = owner->talents.summon_demonic_tyrant->effectN( owner->talents.antoran_armaments.ok() ? 4 : 1 ).misc_value1(); + // NOTE: 2026-04-08 Demonic Tyrant do not trigger Hellbent Commander on demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_demise &= !bugs; + resource_regeneration = regen_type::DISABLED; if ( o()->talents.antoran_armaments.ok() ) action_list_str = "leap/travel/burning_cleave"; @@ -1520,6 +1523,9 @@ grimoire_imp_lord_t::grimoire_imp_lord_t( warlock_t* owner ) npc_id = owner->talents.grimoire_imp_lord->effectN( 2 ).misc_value1(); npc_suffix = "grimoire"; + // NOTE: 2026-04-08 Grimoire: Imp Lord do not trigger Hellbent Commander on demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_demise &= !bugs; + action_list_str = "greater_felbolt,if=energy>=" + util::to_string( max_energy_threshold ); } @@ -1593,6 +1599,9 @@ grimoire_fel_ravager_t::grimoire_fel_ravager_t( warlock_t* owner ) npc_id = owner->talents.grimoire_fel_ravager->effectN( 2 ).misc_value1(); npc_suffix = "grimoire"; + // NOTE: 2026-04-08 Grimoire: Fel Ravager do not trigger Hellbent Commander on demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_demise &= !bugs; + action_list_str = "travel/abyssal_bite,if=energy>=" + util::to_string( max_energy_threshold ); } @@ -1664,11 +1673,14 @@ struct dominion_of_argus_spell_base_t : public warlock_pet_spell_t } }; - dominion_of_argus_pet_t::dominion_of_argus_pet_t( warlock_t* owner, std::string_view n, pet_e type ) +dominion_of_argus_pet_t::dominion_of_argus_pet_t( warlock_t* owner, std::string_view n, pet_e type ) : warlock_pet_t( owner, n, type, true ), main_action( nullptr ) { resource_regeneration = regen_type::DISABLED; affected_by.demonic_brutality = false; + // NOTE: 2026-04-08 DoA guardians do not trigger Hellbent Commander on arise/demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_arise &= !bugs; + triggers.hellbent_commander_demise &= !bugs; } void dominion_of_argus_pet_t::set_main_action( action_t* a ) @@ -1828,7 +1840,7 @@ void antoran_inquisitor_t::create_actions() dominion_of_argus_pet_t::create_actions(); set_main_action( new mind_sear_channel_t( this ) ); } - /// Antoran Inquisitor End +/// Antoran Inquisitor End /// Antoran Jailer Begin struct soul_barrage_t : public warlock_pet_spell_t @@ -2428,15 +2440,43 @@ namespace diabolist is_diabolist_guardian = true; affected_by.demonic_brutality = false; - // NOTE: 2026-02-17 Diabolist guardians do not count towards Hellbent Commander, but they do benefit from its damage increase (bug?) - triggers.hellbent_commander &= !bugs; + // NOTE: 2026-04-08 Diabolist guardians do not trigger Hellbent Commander on arise/demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_arise &= !bugs; + triggers.hellbent_commander_demise &= !bugs; resource_regeneration = regen_type::DISABLED; owner_coeff.ap_from_sp = 1.0; - action_list_str = "wicked_cleave"; + action_list_str = "charge/wicked_cleave"; } + struct overlord_charge_t : warlock_pet_t::travel_t + { + overlord_charge_t( overlord_t* p ) : warlock_pet_t::travel_t( p, "charge" ) + { + speed = 25.0; + } + + bool ready() override + { + // Overlord will do the charge even if is summoned at melee range. In addition, the charge can only occur once. + return debug_cast( player )->charge_executes > 0; + } + + timespan_t execute_time() const override + { + const timespan_t cast_time = 1_s; // TODO: Set cast time from spell data (432113) + return cast_time * player->cache.spell_cast_speed() + warlock_pet_t::travel_t::execute_time(); + } + + void execute() override + { + warlock_pet_t::travel_t::execute(); + + debug_cast( player )->charge_executes--; + } + }; + struct wicked_cleave_t : public warlock_pet_spell_t { wicked_cleave_t( warlock_pet_t* p ) @@ -2456,6 +2496,9 @@ namespace diabolist warlock_pet_spell_t::execute(); debug_cast( p() )->cleaves--; + + if ( debug_cast( p() )->cleaves <= 0 ) + make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } ); } // NOTE: 2026-02-17 Overlord Wicked Cleave crits does not benefit from other crit dmg bonus multipliers (bug?) @@ -2494,6 +2537,7 @@ namespace diabolist { warlock_pet_t::arise(); + charge_executes = 1; cleaves = 1; } @@ -2501,6 +2545,8 @@ namespace diabolist { if ( name == "wicked_cleave" ) return new wicked_cleave_t( this ); + if ( name == "charge" ) + return new overlord_charge_t( this ); return warlock_pet_t::create_action( name, options_str ); } @@ -2516,8 +2562,9 @@ namespace diabolist is_diabolist_guardian = true; affected_by.demonic_brutality = false; - // NOTE: 2026-02-17 Diabolist guardians do not count towards Hellbent Commander, but they do benefit from its damage increase (bug?) - triggers.hellbent_commander &= !bugs; + // NOTE: 2026-04-08 Diabolist guardians do not trigger Hellbent Commander on arise/demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_arise &= !bugs; + triggers.hellbent_commander_demise &= !bugs; action_list_str = "chaos_salvo"; } @@ -2576,6 +2623,14 @@ namespace diabolist debug_cast( p() )->salvos--; } + + void last_tick( dot_t* d ) override + { + warlock_pet_spell_t::last_tick( d ); + + if ( debug_cast( p() )->salvos <= 0 ) + make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } ); + } }; void mother_of_chaos_t::arise() @@ -2600,8 +2655,9 @@ namespace diabolist is_diabolist_guardian = true; affected_by.demonic_brutality = false; - // NOTE: 2026-02-17 Diabolist guardians do not count towards Hellbent Commander, but they do benefit from its damage increase (bug?) - triggers.hellbent_commander &= !bugs; + // NOTE: 2026-04-08 Diabolist guardians do not trigger Hellbent Commander on arise/demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_arise &= !bugs; + triggers.hellbent_commander_demise &= !bugs; resource_regeneration = regen_type::DISABLED; action_list_str = "felseeker"; @@ -2663,6 +2719,14 @@ namespace diabolist debug_cast( p() )->felseekers--; } + + void last_tick( dot_t* d ) override + { + warlock_pet_spell_t::last_tick( d ); + + if ( debug_cast( p() )->felseekers <= 0 ) + make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } ); + } }; void pit_lord_t::arise() @@ -2803,7 +2867,7 @@ struct soul_swipe_t : public soul_swipe_base_t }; rampaging_demonic_soul_t::rampaging_demonic_soul_t( warlock_t* owner, std::string_view name ) - : warlock_pet_t( owner, name, PET_WARLOCK_RANDOM, true ), summon_spell( nullptr ) + : warlock_pet_t( owner, name, PET_WARLOCK_RANDOM, true ) { npc_id = owner->hero.manifested_avarice_spell->effectN( 1 ).misc_value1(); @@ -2811,7 +2875,9 @@ rampaging_demonic_soul_t::rampaging_demonic_soul_t( warlock_t* owner, std::strin affected_by.demonic_brutality = false; action_list_str = "soul_swipe"; owner_coeff.sp_from_sp = 1.0; - summon_spell = owner->find_spell( 1269042 ); // Rampaging Demonic Soul + // NOTE: 2026-04-08 Demonic Soul do not trigger Hellbent Commander on arise/demise (must wait to player heatbeat) (bug?) + triggers.hellbent_commander_arise &= !bugs; + triggers.hellbent_commander_demise &= !bugs; } void rampaging_demonic_soul_t::arise() diff --git a/engine/class_modules/warlock/sc_warlock_pets.hpp b/engine/class_modules/warlock/sc_warlock_pets.hpp index aea2797c46e..b8899b44a42 100644 --- a/engine/class_modules/warlock/sc_warlock_pets.hpp +++ b/engine/class_modules/warlock/sc_warlock_pets.hpp @@ -72,7 +72,8 @@ struct warlock_pet_t : public pet_t struct triggers_t { - bool hellbent_commander; + bool hellbent_commander_arise; + bool hellbent_commander_demise; } triggers; action_t* special_action; // Used for pet interrupts (Axe Toss, Spell Lock) @@ -640,6 +641,7 @@ namespace diabolist { struct overlord_t : public warlock_pet_t { + int charge_executes; int cleaves; overlord_t( warlock_t*, util::string_view = "overlord" ); @@ -686,8 +688,6 @@ namespace soul_harvester { struct rampaging_demonic_soul_t : public warlock_pet_t { - const spell_data_t* summon_spell; - rampaging_demonic_soul_t( warlock_t*, util::string_view = "demonic_soul" ); void arise() override; action_t* create_action( util::string_view, util::string_view ) override;