diff --git a/engine/class_modules/warlock/sc_warlock.cpp b/engine/class_modules/warlock/sc_warlock.cpp index db71e6e52e7..7305cf015fe 100644 --- a/engine/class_modules/warlock/sc_warlock.cpp +++ b/engine/class_modules/warlock/sc_warlock.cpp @@ -59,7 +59,8 @@ warlock_td_t::warlock_td_t( player_t* target, warlock_t& p ) debuffs.lake_of_fire = make_buff( *this, "lake_of_fire", p.talents.lake_of_fire_debuff ) ->set_default_value_from_effect( 1 ) ->set_refresh_behavior( buff_refresh_behavior::DURATION ) - ->set_max_stack( 1 ); + ->set_max_stack( 1 ) + ->set_proc_callbacks( false ); debuffs.shadowburn = make_buff( *this, "shadowburn", p.talents.shadowburn ) ->set_default_value( p.talents.shadowburn_2->effectN( 1 ).base_value() / 10 ); @@ -102,7 +103,8 @@ warlock_td_t::warlock_td_t( player_t* target, warlock_t& p ) ->set_freeze_stacks( true ); debuffs.wither = make_buff( *this, "wither", p.hero.wither_dot ) - ->set_refresh_behavior( buff_refresh_behavior::DURATION ); // Dummy debuff + ->set_refresh_behavior( buff_refresh_behavior::DURATION ) + ->set_proc_callbacks( false ); // Dummy debuff // Soul Harvester dots.soul_anathema = target->get_dot( "soul_anathema", &p ); @@ -195,6 +197,7 @@ warlock_t::warlock_t( sim_t* sim, util::string_view name, race_e r ) havoc_spells(), diabolic_ritual( 0 ), demonic_art_buff_replaced( false ), + wild_imp_ic_shared_offset(), n_active_pets( 0 ), warlock_pet_list( this ), talents(), @@ -240,7 +243,7 @@ warlock_t::warlock_t( sim_t* sim, util::string_view name, race_e r ) const int stack_diff = expected_stacks - current_stacks; if ( stack_diff > 0 ) buffs.hellbent_commander->trigger( stack_diff ); - else if (stack_diff < 0 ) + else if ( stack_diff < 0 ) buffs.hellbent_commander->decrement( std::abs( stack_diff ) ); if ( stack_diff != 0 ) @@ -251,6 +254,14 @@ warlock_t::warlock_t( sim_t* sim, util::string_view name, race_e r ) assert( ( buffs.hellbent_commander->check() == expected_stacks ) && "Incorrent Demon Count for Hellbent Commander" ); } + if ( bugs && talents.fel_armaments.ok() ) + { + // On each Heartbeat, the player periodically applies a hidden Fel Armaments aura to the Felguard, triggering procs + auto active_pet = warlock_pet_list.active; + if ( active_pet && active_pet->pet_type == PET_FELGUARD ) + this->trigger_aura_applied_callbacks( proc_data_entries.fel_armaments_2, active_pet ); + } + for ( auto pet : active_pets ) { auto lock_pet = dynamic_cast( pet ); @@ -305,18 +316,35 @@ void warlock_t::init_assessors() { player_t::init_assessors(); - auto assessor_fn = [ this ]( result_amount_type, action_state_t* s ){ + // Assessor responsible for handling the accumulated damage in SoC for the explosion + auto assessor_soc_fn = [ this ]( result_amount_type, action_state_t* s ) { if ( get_target_data( s->target )->dots.seed_of_corruption->is_ticking() ) accumulate_seed_of_corruption( get_target_data( s->target ), s->result_total ); return assessor::CONTINUE; }; - assessor_out_damage.add( assessor::TARGET_DAMAGE - 1, assessor_fn ); + assessor_out_damage.add( assessor::TARGET_DAMAGE - 1, assessor_soc_fn ); for ( auto pet : pet_list ) { - pet->assessor_out_damage.add( assessor::TARGET_DAMAGE - 1, assessor_fn ); + pet->assessor_out_damage.add( assessor::TARGET_DAMAGE - 1, assessor_soc_fn ); + } + + if ( hero.shared_fate.ok() || hero.feast_of_souls.ok() ) + { + assert( hero.marked_soul->ok() ); + // Assessor used with Soul Harvester to handle proc triggers (trinkets, enchants, ...) from damage-over-time effects + auto assessor_sh_fn = [ this ]( result_amount_type amount_type, action_state_t* s ) { + // Soul Harvester seems to have some hidden trigger tied to damage-over-time effects + // We assume this trigger is Marked Soul and that it is only active when Shared Fate or Feast of Souls is selected + if ( amount_type == result_amount_type::DMG_OVER_TIME ) + trigger_aura_applied_callbacks( proc_data_entries.marked_soul, s->target ); + + return assessor::CONTINUE; + }; + + assessor_out_damage.add ( assessor::TARGET_DAMAGE + 1, assessor_sh_fn ); } } @@ -325,6 +353,42 @@ void warlock_t::init_finished() parse_player_effects(); player_t::init_finished(); + + // 2026-04-06: The Infernal Command (IC) buff is applied/faded periodically every ~5.25 seconds, with some variance. + // The timing of IC buff events starts independently for each imp when it spawns, rather than following a single global + // heartbeat window. However, nearby applications/fades do appear to cluster within small time windows. + // In-game testing suggests this can be modeled fairly closely using a global periodic window (~0.42s) and some variance. + // The current value of this buff is 0, so it does not provide any damage increase. + // It is still relevant, however, because applying the buff can trigger trinkets and other proc effects. + if ( demonology() ) + { + register_combat_begin( [ this ]( player_t* ) { + timespan_t initial_delay = rng().range( 0_ms, 420_ms ); + make_event( sim, initial_delay, [ this ]() { + make_repeating_event( sim, 420_ms, [ this ]() { + auto active_pet = warlock_pet_list.active; + if ( active_pet && active_pet->pet_type == PET_FELGUARD ) + { + wild_imp_ic_shared_offset = timespan_t::from_millis( rng().range( -267, 267 ) ); + auto imps = warlock_pet_list.wild_imps.active_pets(); + for ( auto imp : imps ) + { + if ( sim->current_time() >= ( imp->infernal_command_ev_ts + imp->infernal_command_ev_offset ) ) + { + if ( imp->buffs.infernal_command->check() ) + imp->buffs.infernal_command->expire(); + else + imp->buffs.infernal_command->trigger(); + + imp->infernal_command_ev_ts += 5250_ms; + imp->infernal_command_ev_offset = wild_imp_ic_shared_offset; + } + } + } + } ); + } ); + } ); + } } void warlock_t::invalidate_cache( cache_e c ) @@ -1079,16 +1143,16 @@ void warlock_t::summon_dominion_of_argus_pet( dominion_of_argus_pet_e pet ) switch ( actual_pet ) { case DOA_PET_JAILER: - summon.antoran_jailer->execute(); + summons.antoran_jailer->execute(); break; case DOA_PET_SACROLASH: - summon.lady_sacrolash->execute(); + summons.lady_sacrolash->execute(); break; case DOA_PET_GRAND_WARLOCK: - summon.grand_warlock_alythess->execute(); + summons.grand_warlock_alythess->execute(); break; case DOA_PET_INQUISITOR: - summon.antoran_inquisitor->execute(); + summons.antoran_inquisitor->execute(); break; default: break; @@ -1204,6 +1268,7 @@ warlock::warlock_t::pets_t::pets_t( warlock_t* w ) : active( nullptr ), infernals( "infernal", w ), darkglares( "darkglare", w ), + desperate_souls( "desperate_soul", w ), dreadstalkers( "dreadstalker", w ), vilefiends( "vilefiend", w ), demonic_tyrants( "demonic_tyrant", w ), @@ -1215,7 +1280,7 @@ warlock::warlock_t::pets_t::pets_t( warlock_t* w ) grand_warlock_alythess( "grand_warlock_alythess", w ), antoran_inquisitor( "antoran_inquisitor", w ), antoran_jailer( "antoran_jailer", w ), - shadow_rifts( "shadowy_tear", w ), + shadowy_rifts( "shadowy_tear", w ), unstable_rifts( "unstable_tear", w ), chaos_rifts( "chaos_tear", w ), rocs( "infernal_roc", w ), diff --git a/engine/class_modules/warlock/sc_warlock.hpp b/engine/class_modules/warlock/sc_warlock.hpp index 4d85fc37c69..c721d3b6873 100644 --- a/engine/class_modules/warlock/sc_warlock.hpp +++ b/engine/class_modules/warlock/sc_warlock.hpp @@ -259,6 +259,7 @@ struct warlock_t : public parse_player_effects_t std::vector wild_imp_spawns; // Used for tracking incoming imps from HoG TODO: Is this still needed with faster spawns? int diabolic_ritual; // Used to cycle between the three different Diabolic Ritual buffs bool demonic_art_buff_replaced; // Used to not spawn the Demonic Art demon if the buff is replaced by another + timespan_t wild_imp_ic_shared_offset; // Used as a shared offset when scheduling Wild Imp Infernal Command periodic events unsigned n_active_pets; @@ -270,6 +271,7 @@ struct warlock_t : public parse_player_effects_t const spell_data_t* drain_life; const spell_data_t* corruption; const spell_data_t* shadow_bolt; + const spell_data_t* shadow_bolt_energize; // Affliction const spell_data_t* affliction_warlock; // Spec aura @@ -281,6 +283,7 @@ struct warlock_t : public parse_player_effects_t const spell_data_t* wild_imp; // Data for pet summoning (HoG) const spell_data_t* wild_imp_2; // Data for pet summoning (Inner Demons / Spiteful Reconstitution / To Hell and Back) const spell_data_t* fel_firebolt_2; // Still a separate spell (learned automatically). Reduces pet's energy cost + const spell_data_t* infernal_command_buff; // This still applies but with 0 value // Destruction const spell_data_t* destruction_warlock; // Spec aura @@ -300,6 +303,7 @@ struct warlock_t : public parse_player_effects_t spawner::pet_spawner_t infernals; spawner::pet_spawner_t darkglares; + spawner::pet_spawner_t desperate_souls; spawner::pet_spawner_t dreadstalkers; spawner::pet_spawner_t vilefiends; @@ -313,7 +317,7 @@ struct warlock_t : public parse_player_effects_t spawner::pet_spawner_t antoran_inquisitor; spawner::pet_spawner_t antoran_jailer; - spawner::pet_spawner_t shadow_rifts; + spawner::pet_spawner_t shadowy_rifts; spawner::pet_spawner_t unstable_rifts; spawner::pet_spawner_t chaos_rifts; spawner::pet_spawner_t rocs; @@ -357,6 +361,7 @@ struct warlock_t : public parse_player_effects_t // Affliction player_talent_t agony; + const spell_data_t* agony_energize; player_talent_t unstable_affliction; const spell_data_t* unstable_affliction_2; // Soul Shard on demise (learned automatically) player_talent_t seed_of_corruption; @@ -426,6 +431,7 @@ struct warlock_t : public parse_player_effects_t player_talent_t shadow_of_nathreza_1; player_talent_t shadow_of_nathreza_2; player_talent_t shadow_of_nathreza_3; + const spell_data_t* summon_desperate_soul; const spell_data_t* shadow_of_nathreza_dot; const spell_data_t* wrath_of_nathreza; // Trigger missile spell const spell_data_t* wrath_of_nathreza_impact; @@ -437,10 +443,12 @@ struct warlock_t : public parse_player_effects_t player_talent_t demoniac; const spell_data_t* demonbolt_spell; + const spell_data_t* demonbolt_energize; const spell_data_t* demonic_core_spell; const spell_data_t* demonic_core_buff; player_talent_t call_dreadstalkers; - const spell_data_t* call_dreadstalkers_2; // Contains duration data + const spell_data_t* call_dreadstalkers_summon_1; // Contains summon data + const spell_data_t* call_dreadstalkers_summon_2; // Contains summon data player_talent_t dominant_hand; player_talent_t fel_intellect; @@ -476,7 +484,7 @@ struct warlock_t : public parse_player_effects_t const spell_data_t* tyrants_oblation_buff; player_talent_t antoran_armaments; player_talent_t flametouched; - const spell_data_t* ferocity_of_fharg_buff; + const spell_data_t* flametouched_buff; player_talent_t demonic_knowledge; player_talent_t sacrificed_souls; @@ -713,6 +721,7 @@ struct warlock_t : public parse_player_effects_t player_talent_t shared_fate; const spell_data_t* shared_fate_dot; player_talent_t feast_of_souls; + const spell_data_t* marked_soul; player_talent_t wicked_reaping; const spell_data_t* wicked_reaping_dmg; @@ -752,16 +761,43 @@ struct warlock_t : public parse_player_effects_t action_t* echo_of_sargeras_rof; action_t* embers_of_nihilam; action_t* shadow_of_nathreza; - action_t* wrath_of_nathreza; } proc_actions; struct pet_summons_t { - propagate_const lady_sacrolash; - propagate_const grand_warlock_alythess; - propagate_const antoran_inquisitor; - propagate_const antoran_jailer; - } summon; + action_t* desperate_soul; + action_t* wild_imp; + action_t* wild_imp_2; + action_t* dreadstalker_1; + action_t* dreadstalker_2; + action_t* vilefiend; + action_t* lady_sacrolash; + action_t* grand_warlock_alythess; + action_t* antoran_inquisitor; + action_t* antoran_jailer; + action_t* infernal; + action_t* roc; + action_t* fragment; + action_t* overfiend; + action_t* shadowy_rift; + action_t* unstable_rift; + action_t* chaos_rift; + action_t* overlord; + action_t* mother; + action_t* pit_lord; + action_t* diabolic_imp; + action_t* manifested_demonic_soul; + } summons; + + struct proc_data_entries_t + { + proc_data_t shadow_bolt_energize; + proc_data_t agony_energize; + proc_data_t demonbolt_energize; + proc_data_t incinerate_energize; + proc_data_t fel_armaments_2; + proc_data_t marked_soul; + } proc_data_entries; struct tier_sets_t { @@ -1215,6 +1251,7 @@ struct warlock_t : public parse_player_effects_t void init_rng_soul_harvester(); void init_procs_soul_harvester(); + void init_proc_data_entries(); pet_t* create_main_pet( util::string_view pet_name, util::string_view pet_type ); std::unique_ptr create_pet_expression( util::string_view name_str ); }; diff --git a/engine/class_modules/warlock/sc_warlock_actions.cpp b/engine/class_modules/warlock/sc_warlock_actions.cpp index 02208db3e2d..9986244fca6 100644 --- a/engine/class_modules/warlock/sc_warlock_actions.cpp +++ b/engine/class_modules/warlock/sc_warlock_actions.cpp @@ -256,13 +256,7 @@ using namespace helpers; { if ( p()->deck_rng.rain_of_chaos->trigger() ) { - // Random extra duration time between 0_ms and 820_ms following a uniform distribution - const timespan_t dur_adjust = timespan_t::from_millis( rng().range( 0.0, 820.0 ) ); - auto spawned = p()->warlock_pet_list.rocs.spawn( p()->talents.summon_infernal_roc->duration() + dur_adjust ); - for ( pets::destruction::infernal_t* s : spawned ) - { - s->type = pets::destruction::infernal_t::infernal_type_e::RAIN; - } + p()->summons.roc->execute(); p()->procs.rain_of_chaos->occur(); } } @@ -842,6 +836,9 @@ using namespace helpers; p()->feast_of_souls_gain(); } + // Shadow Bolt energize spell triggers procs + p()->trigger_aura_applied_callbacks( p()->proc_data_entries.shadow_bolt_energize, p() ); + if ( time_to_execute == 0_ms ) p()->buffs.nightfall->decrement(); } @@ -1058,6 +1055,12 @@ using namespace helpers; p()->procs.nightfall->occur(); p()->buffs.nightfall->trigger(); } + if ( p()->talents.siphon_life.ok() || ( p()->hero.seeds_of_their_demise.ok() && d->target->health_percentage() <= p()->hero.seeds_of_their_demise->effectN( 2 ).base_value() ) ) + { + // Affliction Wither DoT ticks trigger procs when talented into Siphon Life + // Affliction Wither DoT ticks also trigger procs when attempting to start a collapse via Seeds of Their Demise + p()->trigger_aura_applied_callbacks( proc_data, p() ); + } } if ( destruction() ) @@ -1067,7 +1070,7 @@ using namespace helpers; p()->resource_gain( RESOURCE_SOUL_SHARD, 0.1, p()->gains.wither ); - if ( p()->talents.flashpoint.ok() && d->state->target->health_percentage() >= p()->talents.flashpoint->effectN( 2 ).base_value() ) + if ( p()->talents.flashpoint.ok() && d->target->health_percentage() >= p()->talents.flashpoint->effectN( 2 ).base_value() ) p()->buffs.flashpoint->trigger(); if ( p()->talents.demonfire_infusion.ok() && p()->flat_rng.demonfire_infusion_dot->trigger() ) @@ -1075,6 +1078,9 @@ using namespace helpers; p()->proc_actions.demonfire_infusion->execute_on_target( d->target ); p()->procs.demonfire_infusion_dot->occur(); } + + // Destruction Wither DoT ticks trigger procs through some hidden trigger + p()->trigger_aura_applied_callbacks( proc_data, p() ); } // Seeds of their Demise collapse conditions must be checked periodically for every Wither tick @@ -1345,6 +1351,25 @@ using namespace helpers; } }; + struct summon_manifested_demonic_soul_t : public warlock_spell_t + { + summon_manifested_demonic_soul_t( warlock_t* p ) + : warlock_spell_t( "Manifested Demonic Soul (Summon)", p, p->hero.manifested_avarice_spell ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.demonic_souls.spawn( data().duration() ); + + p()->buffs.manifested_demonic_soul->trigger(); + } + }; + // Soul Harvester Actions End // Affliction Actions Begin @@ -1413,11 +1438,16 @@ using namespace helpers; void tick( dot_t* d ) override { if ( p()->progress_rng.agony_energize->trigger( d->state ) ) + { p()->resource_gain( RESOURCE_SOUL_SHARD, 1.0, p()->gains.agony ); + // Agony energize spell triggers procs + p()->trigger_aura_applied_callbacks( p()->proc_data_entries.agony_energize, p() ); + } + warlock_spell_t::tick( d ); - td( d->state->target )->dots.agony->increment( 1 ); + d->increment( 1 ); } }; @@ -1455,8 +1485,7 @@ using namespace helpers; if ( p()->hero.manifested_avarice.ok() && p()->prd_rng.manifested_avarice->trigger() ) { - p()->warlock_pet_list.demonic_souls.spawn( p()->hero.manifested_avarice_spell->duration() ); - p()->buffs.manifested_demonic_soul->trigger(); + p()->summons.manifested_demonic_soul->execute(); p()->procs.manifested_avarice->occur(); } @@ -1497,14 +1526,14 @@ using namespace helpers; warlock_spell_t::last_tick( d ); - if ( p()->talents.fatal_echoes.ok() && !d->state->target->is_sleeping() ) + if ( p()->talents.fatal_echoes.ok() && !d->target->is_sleeping() ) { for ( int i = 0; i < stacks; i++ ) { if ( p()->prd_rng.fatal_echoes->trigger() ) { p()->procs.fatal_echoes->occur(); - make_event( sim, 1_ms, [ this, t = d->state->target ] { + make_event( sim, 1_ms, [ this, t = d->target ] { const bool prev_ua_ticking = td( t )->dots.unstable_affliction->is_ticking(); this->set_target( t ); this->is_fatal_echoes_execute = true; @@ -1788,8 +1817,7 @@ using namespace helpers; if ( p()->hero.manifested_avarice.ok() && p()->prd_rng.manifested_avarice->trigger() ) { - p()->warlock_pet_list.demonic_souls.spawn( p()->hero.manifested_avarice_spell->duration() ); - p()->buffs.manifested_demonic_soul->trigger(); + p()->summons.manifested_demonic_soul->execute(); p()->procs.manifested_avarice->occur(); } @@ -1992,7 +2020,7 @@ using namespace helpers; if ( p->talents.unstable_affliction.ok() ) { unstable_affliction_mg = new unstable_affliction_mg_t( p ); - add_child( unstable_affliction_mg); + add_child( unstable_affliction_mg ); } if ( p->hero.wither.ok() ) { @@ -2076,7 +2104,7 @@ using namespace helpers; volley->execute_on_target( d->target ); } - warlock_td_t* tdata = td( d->state->target ); + warlock_td_t* tdata = td( d->target ); if ( !tdata ) return; @@ -2284,7 +2312,7 @@ using namespace helpers; { warlock_spell_t::execute(); - p()->warlock_pet_list.darkglares.spawn( p()->talents.summon_darkglare->duration() ); + p()->warlock_pet_list.darkglares.spawn( data().duration() ); } }; @@ -2367,6 +2395,23 @@ using namespace helpers; } }; + struct summon_desperate_soul_t : public warlock_spell_t + { + summon_desperate_soul_t( warlock_t* p ) + : warlock_spell_t( "Summon Desperate Soul", p, p->talents.summon_desperate_soul ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.desperate_souls.spawn( data().duration() ); + } + }; + struct shadow_of_nathreza_dmg_t : public warlock_spell_t { shadow_of_nathreza_dmg_t( warlock_t* p ) @@ -2376,19 +2421,55 @@ using namespace helpers; } }; - struct wrath_of_nathreza_t : public warlock_spell_t + // Affliction Actions End + // Demonology Actions Begin + + struct summon_wild_imp_base_t : public warlock_spell_t { - wrath_of_nathreza_t( warlock_t* p ) - : warlock_spell_t( "wrath_of_nathreza", p, p->talents.wrath_of_nathreza_impact ) + std::vector last_summoned_imps; + + summon_wild_imp_base_t( util::string_view n, warlock_t* p, const spell_data_t* s = spell_data_t::nil() ) + : warlock_spell_t( n, p, s ) { - background = dual = true; - aoe = -1; - reduced_aoe_targets = as( p->talents.wrath_of_nathreza->effectN( 2 ).base_value() ); + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + last_summoned_imps = p()->warlock_pet_list.wild_imps.spawn( data().duration() ); + + // Wild Imp summon spell triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); + } + + std::vector execute_spawn( unsigned n_imps = 1 ) + { + std::vector imps; + for ( unsigned i = 0; i < n_imps; i++ ) + { + execute(); + imps.insert( imps.end(), last_summoned_imps.begin(), last_summoned_imps.end() ); + } + return imps; } }; - // Affliction Actions End - // Demonology Actions Begin + struct summon_wild_imp_t : public summon_wild_imp_base_t + { + summon_wild_imp_t( warlock_t* p ) + : summon_wild_imp_base_t( "Wild Imp (Summon)", p, p->warlock_base.wild_imp ) + { } + }; + + struct summon_wild_imp_2_t : public summon_wild_imp_base_t + { + summon_wild_imp_2_t( warlock_t* p ) + : summon_wild_imp_base_t( "Wild Imp (Summon) (Alternate)", p, p->warlock_base.wild_imp_2 ) + { } + }; struct hand_of_guldan_t : public warlock_spell_t { @@ -2658,8 +2739,7 @@ using namespace helpers; if ( p()->hero.manifested_avarice.ok() && p()->prd_rng.manifested_avarice->trigger() ) { - p()->warlock_pet_list.demonic_souls.spawn( p()->hero.manifested_avarice_spell->duration() ); - p()->buffs.manifested_demonic_soul->trigger(); + p()->summons.manifested_demonic_soul->execute(); p()->procs.manifested_avarice->occur(); } @@ -2742,7 +2822,7 @@ using namespace helpers; { if ( p()->talents.spiteful_reconstitution.ok() && p()->prd_rng.spiteful_reconstitution->trigger() ) { - p()->warlock_pet_list.wild_imps.spawn( p()->warlock_base.wild_imp_2->duration(), 1u ); + p()->summons.wild_imp_2->execute(); p()->procs.spiteful_reconstitution->occur(); } } @@ -2762,6 +2842,9 @@ using namespace helpers; if ( p()->talents.summon_doomguard.ok() && p()->buffs.demonic_core->check() ) p()->cooldowns.summon_doomguard->adjust( timespan_t::from_seconds( -p()->talents.summon_doomguard->effectN( 2 ).base_value() ) ); + // Demonbolt energize spell triggers procs + p()->trigger_aura_applied_callbacks( p()->proc_data_entries.demonbolt_energize, p() ); + p()->buffs.demonic_core->decrement(); p()->buffs.power_siphon->decrement(); @@ -2788,7 +2871,6 @@ using namespace helpers; { aoe = -1; background = dual = true; - callbacks = false; } double action_multiplier() const override @@ -2891,7 +2973,7 @@ using namespace helpers; unsigned new_imps = ( launch_counter / as( p()->talents.to_hell_and_back->effectN( 2 ).base_value() ) ) * as( p()->talents.to_hell_and_back->effectN( 1 ).base_value() ); if ( new_imps > 0 ) { - auto imps = p()->warlock_pet_list.wild_imps.spawn( p()->warlock_base.wild_imp_2->duration(), new_imps ); + auto imps = debug_cast( p()->summons.wild_imp_2 )->execute_spawn( new_imps ); for ( auto imp : imps ) { imp->buffs.imp_gang_boss->trigger(); @@ -2901,6 +2983,9 @@ using namespace helpers; } warlock_spell_t::execute(); + + // Implosion cast triggers procs through some hidden trigger + p()->trigger_aura_applied_callbacks( proc_data, p() ); } timespan_t calc_imp_travel_time( double speed ) @@ -2926,13 +3011,13 @@ using namespace helpers; } }; - struct summon_vilefiend_t : public warlock_spell_t + struct summon_vilefiend_base_t : public warlock_spell_t { - summon_vilefiend_t( warlock_t* p ) - : warlock_spell_t( "Summon Vilefiend", p, p->talents.vilefiend ) + summon_vilefiend_base_t( util::string_view n, warlock_t* p, const spell_data_t* s = spell_data_t::nil() ) + : warlock_spell_t( n, p, s ) { - background = dual = true; harmful = may_crit = false; + background = true; } void execute() override @@ -2943,18 +3028,84 @@ using namespace helpers; } }; - struct call_dreadstalkers_t : public warlock_spell_t + struct summon_vilefiend_t : public summon_vilefiend_base_t { - summon_vilefiend_t* summon_vilefiend; + summon_vilefiend_t( warlock_t* p ) + : summon_vilefiend_base_t( "Summon Vilefiend", p, p->talents.vilefiend ) + { } + }; + + struct summon_gloomhound_t : public summon_vilefiend_base_t + { + summon_gloomhound_t( warlock_t* p ) + : summon_vilefiend_base_t( "Summon Gloomhound", p, p->talents.gloomhound ) + { } + }; + + struct summon_charhound_t : public summon_vilefiend_base_t + { + summon_charhound_t( warlock_t* p ) + : summon_vilefiend_base_t( "Summon Charhound", p, p->talents.charhound ) + { } + }; + + struct summon_dreadstalker_base_t : public warlock_spell_t + { + timespan_t dur_adjust; + timespan_t server_action_delay; + + summon_dreadstalker_base_t( util::string_view n, warlock_t* p, const spell_data_t* s = spell_data_t::nil() ) + : warlock_spell_t( n, p, s ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + auto dogs = p()->warlock_pet_list.dreadstalkers.spawn( data().duration() + dur_adjust ); + + for ( auto dog : dogs ) + { + if ( dog->is_active() ) + dog->server_action_delay = server_action_delay; + } + + // Call Dreadstalkers summon spell triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); + } + + void execute( timespan_t dur_adjust_, timespan_t server_action_delay_ ) + { + dur_adjust = dur_adjust_; + server_action_delay = server_action_delay_; + execute(); + } + }; + + struct summon_dreadstalker_1_t : public summon_dreadstalker_base_t + { + summon_dreadstalker_1_t( warlock_t* p ) + : summon_dreadstalker_base_t( "Call Dreadstalkers (Summon) (1)", p, p->talents.call_dreadstalkers_summon_1 ) + { } + }; + + struct summon_dreadstalker_2_t : public summon_dreadstalker_base_t + { + summon_dreadstalker_2_t( warlock_t* p ) + : summon_dreadstalker_base_t( "Call Dreadstalkers (Summon) (2)", p, p->talents.call_dreadstalkers_summon_2 ) + { } + }; + struct call_dreadstalkers_t : public warlock_spell_t + { call_dreadstalkers_t( warlock_t* p, util::string_view options_str ) : warlock_spell_t( "Call Dreadstalkers", p, p->talents.call_dreadstalkers, options_str ) { may_crit = false; triggers.diabolic_ritual = p->hero.diabolic_ritual.ok(); - - if ( p->talents.summon_vilefiend.ok() ) - summon_vilefiend = new summon_vilefiend_t( p ); } void execute() override @@ -2967,18 +3118,16 @@ using namespace helpers; const timespan_t& delay = delay_dur_adjusts.first; const timespan_t& dur_adjust = delay_dur_adjusts.second; - auto dogs = p()->warlock_pet_list.dreadstalkers.spawn( p()->talents.call_dreadstalkers_2->duration() + dur_adjust, count ); - - for ( auto d : dogs ) + for ( unsigned i = 0; i < count; i++ ) { - if ( d->is_active() ) - { - d->server_action_delay = delay; - } + summon_dreadstalker_base_t* summon_dreadstalker_action = i ? debug_cast( p()->summons.dreadstalker_2 ) + : debug_cast( p()->summons.dreadstalker_1 ); + + summon_dreadstalker_action->execute( dur_adjust, delay ); } if ( p()->talents.summon_vilefiend.ok() ) - summon_vilefiend->execute_on_target( target ); + p()->summons.vilefiend->execute_on_target( target ); } }; @@ -3097,7 +3246,7 @@ using namespace helpers; unsigned new_imps = ( sac_counter / as( p()->talents.to_hell_and_back->effectN( 2 ).base_value() ) ) * as( p()->talents.to_hell_and_back->effectN( 1 ).base_value() ); if ( new_imps > 0 ) { - auto imps = p()->warlock_pet_list.wild_imps.spawn( p()->warlock_base.wild_imp_2->duration(), new_imps ); + auto imps = debug_cast( p()->summons.wild_imp_2 )->execute_spawn( new_imps ); for ( auto imp : imps ) { imp->buffs.imp_gang_boss->trigger(); @@ -3220,6 +3369,9 @@ using namespace helpers; warlock_spell_t::execute(); p()->warlock_pet_list.grimoire_imp_lords.spawn( data().duration() ); + + // Grimoire: Imp Lord summon spell triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); } }; @@ -3238,6 +3390,9 @@ using namespace helpers; warlock_spell_t::execute(); p()->warlock_pet_list.grimoire_fel_ravagers.spawn( data().duration() ); + + // Grimoire: Fel Ravager summon spell triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); } }; @@ -3256,6 +3411,9 @@ using namespace helpers; warlock_spell_t::execute(); p()->warlock_pet_list.doomguards.spawn( data().duration() ); + + // Summon Doomguard summon spell triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); } }; @@ -3446,6 +3604,9 @@ using namespace helpers; p()->procs.demonfire_infusion_inc->occur(); } + // Incinerate energize spell triggers procs + p()->trigger_aura_applied_callbacks( p()->proc_data_entries.incinerate_energize, p() ); + // Backdraft is not consumed by an instant Incinerate cast benefiting from Chaotic Inferno // NOTE: To achieve this, the game checks if the player has the Chaotic Inferno buff bool consume_backdraft = p()->bugs ? !p()->buffs.chaotic_inferno->check() : ( time_to_execute != 0_ms ); @@ -3493,7 +3654,7 @@ using namespace helpers; p()->resource_gain( RESOURCE_SOUL_SHARD, 0.1, p()->gains.immolate ); - if ( p()->talents.flashpoint.ok() && d->state->target->health_percentage() >= p()->talents.flashpoint->effectN( 2 ).base_value() ) + if ( p()->talents.flashpoint.ok() && d->target->health_percentage() >= p()->talents.flashpoint->effectN( 2 ).base_value() ) p()->buffs.flashpoint->trigger(); if ( p()->talents.demonfire_infusion.ok() && p()->flat_rng.demonfire_infusion_dot->trigger() ) @@ -3501,6 +3662,9 @@ using namespace helpers; p()->proc_actions.demonfire_infusion->execute_on_target( d->target ); p()->procs.demonfire_infusion_dot->occur(); } + + // Immolate DoT ticks trigger procs through some hidden trigger + p()->trigger_aura_applied_callbacks( proc_data, p() ); } }; @@ -3557,7 +3721,7 @@ using namespace helpers; warlock_spell_t::execute(); dot->adjust_duration( -remaining ); - if ( p()->hero.wither.ok() ) + if ( p()->hero.wither.ok() && remaining != 0_ms ) { auto& wither_debuff = td( target )->debuffs.wither; if ( wither_debuff->remains() - remaining <= timespan_t::zero() ) @@ -3570,6 +3734,76 @@ using namespace helpers; } }; + struct summon_shadowy_tear_t : public warlock_spell_t + { + summon_shadowy_tear_t( warlock_t* p ) + : warlock_spell_t( "Shadowy Tear (Summon)", p, p->talents.shadowy_tear_summon ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.shadowy_rifts.spawn( data().duration() ); + } + }; + + struct summon_unstable_tear_t : public warlock_spell_t + { + summon_unstable_tear_t( warlock_t* p ) + : warlock_spell_t( "Unstable Tear (Summon)", p, p->talents.unstable_tear_summon ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.unstable_rifts.spawn( data().duration() ); + } + }; + + struct summon_chaos_tear_t : public warlock_spell_t + { + summon_chaos_tear_t( warlock_t* p ) + : warlock_spell_t( "Chaos Tear (Summon)", p, p->talents.chaos_tear_summon ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.chaos_rifts.spawn( data().duration() ); + } + }; + + struct summon_overfiend_t : public warlock_spell_t + { + summon_overfiend_t( warlock_t* p ) + : warlock_spell_t( "Summon Overfiend", p, p->talents.summon_overfiend ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.overfiends.spawn( data().duration() ); + + p()->buffs.summon_overfiend->trigger(); + } + }; + struct dimensional_rift_t : public warlock_spell_t { dimensional_rift_t( warlock_t* p ) @@ -3587,20 +3821,19 @@ using namespace helpers; switch ( rift_pet_index ) { case DR_PET_SHADOWY_TEAR: - p()->warlock_pet_list.shadow_rifts.spawn( p()->talents.shadowy_tear_summon->duration() ); + p()->summons.shadowy_rift->execute(); p()->procs.dimensional_rift->occur(); break; case DR_PET_UNSTABLE_TEAR: - p()->warlock_pet_list.unstable_rifts.spawn( p()->talents.unstable_tear_summon->duration() ); + p()->summons.unstable_rift->execute(); p()->procs.dimensional_rift->occur(); break; case DR_PET_CHAOS_TEAR: - p()->warlock_pet_list.chaos_rifts.spawn( p()->talents.chaos_tear_summon->duration() ); + p()->summons.chaos_rift->execute(); p()->procs.dimensional_rift->occur(); break; case DR_PET_OVERFIEND: - p()->warlock_pet_list.overfiends.spawn(); - p()->buffs.summon_overfiend->trigger(); + p()->summons.overfiend->execute(); p()->procs.avatar_of_destruction->occur(); default: break; @@ -3975,6 +4208,9 @@ using namespace helpers; .start_time( sim->current_time() ) .action( p()->proc_actions.rain_of_fire_tick ) ); + // Rain of Fire spell cast triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); + if ( p()->talents.embers_of_nihilam_3.ok() ) helpers::trigger_echo_of_sargeras( p(), execute_state->target, p()->proc_actions.echo_of_sargeras_rof, p()->procs.echo_of_sargeras_rof ); @@ -4376,6 +4612,65 @@ using namespace helpers; } }; + struct summon_main_infernal_pet_t : public warlock_spell_t + { + summon_main_infernal_pet_t( warlock_t* p ) + : warlock_spell_t( "Summon Infernal (Summon) (Main)", p, p->talents.summon_infernal_main ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + // Random extra duration time between 0_ms and 820_ms following a uniform distribution + const timespan_t dur_adjust = timespan_t::from_millis( rng().range( 0.0, 820.0 ) ); + p()->warlock_pet_list.infernals.spawn( data().duration() + dur_adjust ); + } + }; + + struct summon_roc_infernal_pet_t : public warlock_spell_t + { + summon_roc_infernal_pet_t( warlock_t* p ) + : warlock_spell_t( "Summon Infernal (Summon) (Roc)", p, p->talents.summon_infernal_roc ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + // Random extra duration time between 0_ms and 820_ms following a uniform distribution + const timespan_t dur_adjust = timespan_t::from_millis( rng().range( 0.0, 820.0 ) ); + auto spawned = p()->warlock_pet_list.rocs.spawn( data().duration() + dur_adjust ); + for ( pets::destruction::infernal_t* s : spawned ) + s->type = pets::destruction::infernal_t::infernal_type_e::RAIN; + } + }; + + struct summon_fragment_infernal_pet_t : public warlock_spell_t + { + summon_fragment_infernal_pet_t( warlock_t* p ) + : warlock_spell_t( "Infernal Fragmentation (Summon) (Fragment)", p, p->hero.infernal_fragmentation ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + // Random extra duration time between 0_ms and 820_ms following a uniform distribution + const timespan_t dur_adjust = timespan_t::from_millis( rng().range( 0.0, 820.0 ) ); + p()->warlock_pet_list.fragments.spawn( data().duration() + dur_adjust ); + } + }; + struct infernal_awakening_t : public warlock_spell_t { infernal_awakening_t( warlock_t* p ) @@ -4389,9 +4684,7 @@ using namespace helpers; { warlock_spell_t::execute(); - // Random extra duration time between 0_ms and 820_ms following a uniform distribution - const timespan_t dur_adjust = timespan_t::from_millis( rng().range( 0.0, 820.0 ) ); - p()->warlock_pet_list.infernals.spawn( p()->talents.summon_infernal_main->duration() + dur_adjust ); + p()->summons.infernal->execute(); } }; @@ -4465,8 +4758,7 @@ using namespace helpers; if ( p()->talents.avatar_of_destruction.ok() ) { - p()->warlock_pet_list.overfiends.spawn(); - p()->buffs.summon_overfiend->trigger(); + p()->summons.overfiend->execute(); p()->procs.avatar_of_destruction->occur(); } } @@ -4568,6 +4860,9 @@ using namespace helpers; p()->procs.demonfire_infusion_inc->occur(); } + // Infernal Bolt energize spell effect triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); + p()->buffs.infernal_bolt->decrement(); p()->buffs.backdraft->decrement(); @@ -4616,7 +4911,8 @@ using namespace helpers; if ( destruction() ) { - p()->warlock_pet_list.diabolic_imps.spawn( as( p()->hero.ruination_buff->effectN( 3 ).base_value() ) ); + for ( int i = 0; i < as( p()->hero.ruination_buff->effectN( 3 ).base_value() ); i++ ) + p()->summons.diabolic_imp->execute(); } } } @@ -4741,6 +5037,83 @@ using namespace helpers; } }; + struct summon_overlord_t : public warlock_spell_t + { + summon_overlord_t( warlock_t* p ) + : warlock_spell_t( "Summon Overlord", p, p->hero.summon_overlord ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.overlords.spawn( data().duration() ); + } + }; + + struct summon_mother_of_chaos_t : public warlock_spell_t + { + summon_mother_of_chaos_t( warlock_t* p ) + : warlock_spell_t( "Summon Mother of Chaos (Summon)", p, p->hero.summon_mother ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.mothers.spawn( data().duration() ); + + if ( p()->hero.secrets_of_the_coven.ok() ) + p()->buffs.infernal_bolt->trigger(); + } + }; + + struct summon_pit_lord_t : public warlock_spell_t + { + summon_pit_lord_t( warlock_t* p ) + : warlock_spell_t( "Summon Pit Lord", p, p->hero.summon_pit_lord ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.pit_lords.spawn( data().duration() ); + + if ( p()->hero.ruination.ok() ) + p()->buffs.ruination->trigger(); + } + }; + + struct summon_diabolic_imp_t : public warlock_spell_t + { + summon_diabolic_imp_t( warlock_t* p ) + : warlock_spell_t( "Diabolic Imp (Summon)", p, p->hero.diabolic_imp ) + { + harmful = may_crit = false; + background = true; + } + + void execute() override + { + warlock_spell_t::execute(); + + p()->warlock_pet_list.diabolic_imps.spawn( data().duration() ); + + // Diabolic Imp summon spell triggers procs + p()->trigger_aura_applied_callbacks( proc_data, p() ); + } + }; + // Diabolist Actions End // Helper Functions Begin @@ -4865,7 +5238,7 @@ using namespace helpers; if ( !p->rppm_rng.wrath_of_nathreza->trigger() ) return; - p->proc_actions.wrath_of_nathreza->execute_on_target( target ); + p->summons.desperate_soul->execute_on_target( target ); p->procs.wrath_of_nathreza->occur(); } @@ -4883,7 +5256,7 @@ using namespace helpers; { warlock_t* p = static_cast( player() ); - auto imps = p->warlock_pet_list.wild_imps.spawn(); + auto imps = debug_cast( p->summons.wild_imp )->execute_spawn(); if ( p->talents.imp_gang_boss.ok() && index == 0 ) { @@ -4920,7 +5293,7 @@ using namespace helpers; // 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->state->target; + player_t* target = dot->target; dot->decrement( 1 ); assert( ( dot->is_ticking() && dot->current_stack() > 0 ) && "UA stack decrement event should not cancel the DoT" ); @@ -5153,17 +5526,29 @@ using namespace helpers; proc_actions.shadow_of_nathreza = new shadow_of_nathreza_dmg_t( this ); if ( talents.shadow_of_nathreza_3.ok() ) - proc_actions.wrath_of_nathreza = new wrath_of_nathreza_t( this ); + summons.desperate_soul = new summon_desperate_soul_t( this ); } void warlock_t::create_demonology_proc_actions() { - proc_actions.doom_proc = new doom_t( this ); - proc_actions.blighted_maw = new blighted_maw_t( this ); - summon.antoran_inquisitor = get_action( "dominion_of_argus_antoran_inquisitor", this ); - summon.antoran_jailer = get_action( "dominion_of_argus_antoran_jailer", this ); - summon.lady_sacrolash = get_action( "dominion_of_argus_lady_sacrolash", this ); - summon.grand_warlock_alythess = + proc_actions.doom_proc = new doom_t( this ); + proc_actions.blighted_maw = new blighted_maw_t( this ); + summons.wild_imp = new summon_wild_imp_t( this ); + summons.wild_imp_2 = new summon_wild_imp_2_t( this ); + summons.dreadstalker_1 = new summon_dreadstalker_1_t( this ); + summons.dreadstalker_2 = new summon_dreadstalker_2_t( this ); + + if ( talents.mark_of_shatug.ok() ) + summons.vilefiend = new summon_gloomhound_t( this ); + else if ( talents.mark_of_fharg.ok() ) + summons.vilefiend = new summon_charhound_t( this ); + else + summons.vilefiend = new summon_vilefiend_t( this ); + + summons.antoran_inquisitor = get_action( "dominion_of_argus_antoran_inquisitor", this ); + summons.antoran_jailer = get_action( "dominion_of_argus_antoran_jailer", this ); + summons.lady_sacrolash = get_action( "dominion_of_argus_lady_sacrolash", this ); + summons.grand_warlock_alythess = get_action( "dominion_of_argus_grand_warlock_alythess", this ); } @@ -5185,6 +5570,14 @@ using namespace helpers; if ( talents.embers_of_nihilam_1.ok() || talents.embers_of_nihilam_3.ok() ) proc_actions.embers_of_nihilam = new embers_of_nihilam_t( this ); + + summons.infernal = new summon_main_infernal_pet_t( this ); + summons.roc = new summon_roc_infernal_pet_t( this ); + summons.fragment = new summon_fragment_infernal_pet_t( this ); + summons.shadowy_rift = new summon_shadowy_tear_t( this ); + summons.unstable_rift = new summon_unstable_tear_t( this ); + summons.chaos_rift = new summon_chaos_tear_t( this ); + summons.overfiend = new summon_overfiend_t( this ); } void warlock_t::create_diabolist_proc_actions() @@ -5194,6 +5587,10 @@ using namespace helpers; proc_actions.diabolic_gaze_2 = new diabolic_gaze_2_t( this ); proc_actions.diabolic_gaze_3 = new diabolic_gaze_3_t( this ); proc_actions.diabolic_oculi = new diabolic_oculi_t( this ); + summons.overlord = new summon_overlord_t( this ); + summons.mother = new summon_mother_of_chaos_t( this ); + summons.pit_lord = new summon_pit_lord_t( this ); + summons.diabolic_imp = new summon_diabolic_imp_t( this ); } void warlock_t::create_hellcaller_proc_actions() @@ -5207,6 +5604,7 @@ using namespace helpers; proc_actions.demonic_soul = new demonic_soul_t( this ); proc_actions.shared_fate = new shared_fate_t( this ); proc_actions.wicked_reaping = new wicked_reaping_t( this ); + summons.manifested_demonic_soul = new summon_manifested_demonic_soul_t( this ); } void warlock_t::init_special_effects() @@ -5226,7 +5624,7 @@ using namespace helpers; cb->initialize(); cb->deactivate(); - buffs.grimoire_of_sacrifice->set_stack_change_callback( [ cb ]( buff_t*, int, int new_ ){ + buffs.grimoire_of_sacrifice->set_stack_change_callback( [ cb ]( buff_t*, int, int new_ ) { if ( new_ == 1 ) cb->activate(); else cb->deactivate(); } ); diff --git a/engine/class_modules/warlock/sc_warlock_init.cpp b/engine/class_modules/warlock/sc_warlock_init.cpp index 95c4144f57a..742d56ac53f 100644 --- a/engine/class_modules/warlock/sc_warlock_init.cpp +++ b/engine/class_modules/warlock/sc_warlock_init.cpp @@ -32,6 +32,8 @@ namespace warlock warlock_base.wild_imp = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 104317 ); // Contains pet summoning information (HoG) warlock_base.wild_imp_2 = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 279910 ); // Pet summoning information for Inner Demons, Spiteful Reconstitution and To Hell and Back warlock_base.fel_firebolt_2 = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 334591 ); // 20% cost reduction for Wild Imps + warlock_base.infernal_command_buff = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 387552 ); // Buff of an old talent that still applies but with 0 value + warlock_base.shadow_bolt_energize = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 194192 ); // Used for resource gain // Destruction warlock_base.destruction_warlock = find_specialization_spell( "Destruction Warlock", WARLOCK_DESTRUCTION ); // Should be ID 137046 @@ -70,6 +72,8 @@ namespace warlock warlock_t::init_spells_hellcaller(); warlock_t::init_spells_soul_harvester(); + warlock_t::init_proc_data_entries(); + // Register passives // NOTE: 2026-02-17 Currently Gloom of Nathreza talent is bugged for Destruction and does not work if ( destruction() && bugs ) @@ -131,6 +135,7 @@ namespace warlock { // Talents talents.agony = find_talent_spell( talent_tree::SPECIALIZATION, "Agony" ); // Should be ID 980 + talents.agony_energize = conditional_spell_lookup( talents.agony.ok(), 17941 ); talents.unstable_affliction = find_talent_spell( talent_tree::SPECIALIZATION, "Unstable Affliction" ); // Should be ID 1259790 talents.unstable_affliction_2 = conditional_spell_lookup( talents.unstable_affliction.ok(), 231791 ); // Soul Shard on demise @@ -227,6 +232,7 @@ namespace warlock talents.shadow_of_nathreza_2 = find_talent_spell( talent_tree::SPECIALIZATION, "Shadow of Nathreza", 2 ); // Should be ID 1261990 (II) talents.shadow_of_nathreza_3 = find_talent_spell( talent_tree::SPECIALIZATION, "Shadow of Nathreza", 3 ); // Should be ID 1261992 (III) talents.shadow_of_nathreza_dot = conditional_spell_lookup( talents.shadow_of_nathreza_1.ok(), 1262710 ); + talents.summon_desperate_soul = conditional_spell_lookup( talents.shadow_of_nathreza_3.ok(), 1262094 ); talents.wrath_of_nathreza = conditional_spell_lookup( talents.shadow_of_nathreza_3.ok(), 1262028 ); talents.wrath_of_nathreza_impact = conditional_spell_lookup( talents.shadow_of_nathreza_3.ok(), 1278047 ); @@ -236,6 +242,7 @@ namespace warlock // Initialize some default values for pet spawners warlock_pet_list.darkglares.set_default_duration( talents.summon_darkglare->duration() ); + warlock_pet_list.desperate_souls.set_default_duration( talents.summon_desperate_soul->duration() ); } void warlock_t::init_spells_demonology() @@ -247,11 +254,13 @@ namespace warlock talents.demoniac = find_talent_spell( talent_tree::SPECIALIZATION, "Demoniac" ); // Should be ID 426115 talents.demonbolt_spell = conditional_spell_lookup( talents.demoniac.ok(), 264178 ); + talents.demonbolt_energize = conditional_spell_lookup( talents.demoniac.ok(), 280127 ); talents.demonic_core_spell = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 267102 ); talents.demonic_core_buff = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 264173 ); talents.call_dreadstalkers = find_talent_spell( talent_tree::SPECIALIZATION, "Call Dreadstalkers" ); // Should be ID 104316 - talents.call_dreadstalkers_2 = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 193332 ); // Duration data + talents.call_dreadstalkers_summon_1 = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 193331 ); // Summon data + talents.call_dreadstalkers_summon_2 = conditional_spell_lookup( warlock_base.demonology_warlock->ok(), 193332 ); // Summon data talents.dominant_hand = find_talent_spell( talent_tree::SPECIALIZATION, "Dominant Hand" ); // Should be ID 1276433 @@ -304,7 +313,7 @@ namespace warlock talents.antoran_armaments = find_talent_spell( talent_tree::SPECIALIZATION, "Antoran Armaments" ); // Should be ID 1250921 talents.flametouched = find_talent_spell( talent_tree::SPECIALIZATION, "Flametouched" ); // Should be ID 453699 - talents.ferocity_of_fharg_buff = conditional_spell_lookup( talents.flametouched.ok(), 453704 ); + talents.flametouched_buff = conditional_spell_lookup( talents.flametouched.ok(), 453704 ); talents.demonic_knowledge = find_talent_spell( talent_tree::SPECIALIZATION, "Demonic Knowledge" ); // Should be ID 386185 @@ -368,7 +377,7 @@ namespace warlock // Initialize some default values for pet spawners warlock_pet_list.wild_imps.set_default_duration( warlock_base.wild_imp->duration() ); - warlock_pet_list.dreadstalkers.set_default_duration( talents.call_dreadstalkers_2->duration() ); + warlock_pet_list.dreadstalkers.set_default_duration( talents.call_dreadstalkers_summon_2->duration() ); warlock_pet_list.demonic_tyrants.set_default_duration( talents.summon_demonic_tyrant->duration() ); warlock_pet_list.grimoire_imp_lords.set_default_duration( talents.grimoire_imp_lord->duration() ); warlock_pet_list.grimoire_fel_ravagers.set_default_duration( talents.grimoire_fel_ravager->duration() ); @@ -519,7 +528,7 @@ namespace warlock // Initialize some default values for pet spawners warlock_pet_list.infernals.set_default_duration( talents.summon_infernal_main->duration() ); warlock_pet_list.rocs.set_default_duration( talents.summon_infernal_roc->duration() ); - warlock_pet_list.shadow_rifts.set_default_duration( talents.shadowy_tear_summon->duration() ); + warlock_pet_list.shadowy_rifts.set_default_duration( talents.shadowy_tear_summon->duration() ); warlock_pet_list.unstable_rifts.set_default_duration( talents.unstable_tear_summon->duration() ); warlock_pet_list.chaos_rifts.set_default_duration( talents.chaos_tear_summon->duration() ); warlock_pet_list.overfiends.set_default_duration( talents.summon_overfiend->duration() ); @@ -648,6 +657,7 @@ namespace warlock hero.shared_fate_dot = conditional_spell_lookup( hero.shared_fate.ok(), 450591 ); hero.feast_of_souls = find_talent_spell( talent_tree::HERO, "Feast of Souls" ); // Should be ID 449706 + hero.marked_soul = conditional_spell_lookup( hero.shared_fate.ok() || hero.feast_of_souls.ok(), 450629 ); hero.wicked_reaping = find_talent_spell( talent_tree::HERO, "Wicked Reaping" ); // Should be ID 449631 hero.wicked_reaping_dmg = conditional_spell_lookup( hero.wicked_reaping.ok(), 449826 ); @@ -670,6 +680,16 @@ namespace warlock warlock_pet_list.demonic_souls.set_default_duration( hero.manifested_avarice_spell->duration() ); } + void warlock_t::init_proc_data_entries() + { + proc_data_entries.shadow_bolt_energize = warlock_base.shadow_bolt_energize; + proc_data_entries.agony_energize = talents.agony_energize; + proc_data_entries.demonbolt_energize = talents.demonbolt_energize; + proc_data_entries.incinerate_energize = warlock_base.incinerate_energize; + proc_data_entries.fel_armaments_2 = talents.fel_armaments_2; + proc_data_entries.marked_soul = hero.marked_soul; + } + void warlock_t::init_base_stats() { if ( base.distance < 1.0 ) @@ -705,7 +725,9 @@ namespace warlock buffs.soulburn = make_buff( this, "soulburn", talents.soulburn_buff ); - buffs.pet_movement = make_buff( this, "pet_movement" )->set_max_stack( 100 ); + buffs.pet_movement = make_buff( this, "pet_movement" ) + ->set_max_stack( 100 ) + ->set_proc_callbacks( false ); // Affliction buffs create_buffs_affliction(); @@ -748,7 +770,7 @@ namespace warlock ->set_tick_time_behavior( buff_tick_time_behavior::UNHASTED ) ->set_tick_zero( true ) ->set_tick_callback( [ this ]( buff_t*, int, timespan_t ) { - warlock_pet_list.wild_imps.spawn( warlock_base.wild_imp_2->duration(), 1u ); + summons.wild_imp_2->execute(); } ); buffs.tyrants_oblation = make_buff( this, "tyrants_oblation", talents.tyrants_oblation_buff ) @@ -769,25 +791,32 @@ namespace warlock } ); // Pet tracking buffs - buffs.wild_imps = make_buff( this, "wild_imps" )->set_max_stack( 40 ); + buffs.wild_imps = make_buff( this, "wild_imps" )->set_max_stack( 40 ) + ->set_proc_callbacks( false ); buffs.dreadstalkers = make_buff( this, "dreadstalkers" )->set_max_stack( 8 ) - ->set_duration( talents.call_dreadstalkers_2->duration() ); + ->set_duration( talents.call_dreadstalkers_summon_2->duration() ) + ->set_proc_callbacks( false ); buffs.vilefiend = make_buff( this, "vilefiend" )->set_max_stack( 2 ) - ->set_duration( talents.vilefiend->duration() ); + ->set_duration( talents.vilefiend->duration() ) + ->set_proc_callbacks( false ); buffs.grimoire_imp_lord = make_buff( this, "grimoire_imp_lord" )->set_max_stack( 1 ) - ->set_duration( talents.grimoire_imp_lord->duration() ); + ->set_duration( talents.grimoire_imp_lord->duration() ) + ->set_proc_callbacks( false ); buffs.grimoire_fel_ravager = make_buff( this, "grimoire_fel_ravager" )->set_max_stack( 1 ) - ->set_duration( talents.grimoire_fel_ravager->duration() ); + ->set_duration( talents.grimoire_fel_ravager->duration() ) + ->set_proc_callbacks( false ); buffs.doomguard = make_buff( this, "doomguard" )->set_max_stack( 4 ) - ->set_duration( talents.summon_doomguard->duration() ); + ->set_duration( talents.summon_doomguard->duration() ) + ->set_proc_callbacks( false ); buffs.tyrant = make_buff( this, "tyrant" )->set_max_stack( 1 ) - ->set_duration( talents.summon_demonic_tyrant->duration() ); + ->set_duration( talents.summon_demonic_tyrant->duration() ) + ->set_proc_callbacks( false ); } void warlock_t::create_buffs_destruction() @@ -884,7 +913,7 @@ namespace warlock { if ( cur == 0 && in_combat && !demonic_art_buff_replaced ) { - warlock_pet_list.overlords.spawn(); + summons.overlord->execute(); } } ); @@ -894,10 +923,7 @@ namespace warlock { if ( cur == 0 && in_combat && !demonic_art_buff_replaced ) { - warlock_pet_list.mothers.spawn(); - - if ( hero.secrets_of_the_coven.ok() ) - buffs.infernal_bolt->trigger(); + summons.mother->execute(); } } ); @@ -907,10 +933,7 @@ namespace warlock { if ( cur == 0 && in_combat && !demonic_art_buff_replaced ) { - warlock_pet_list.pit_lords.spawn(); - - if ( hero.ruination.ok() ) - buffs.ruination->trigger(); + summons.pit_lord->execute(); } } ); @@ -1374,7 +1397,7 @@ namespace warlock assert( 100u % chance == 0u ); const unsigned alythesss_ire_trigger = 100u / chance; - cycle_proc.alythesss_ire = get_rng( "alythesss_ire", alythesss_ire_trigger, true, [ this ]( unsigned trigger_count ){ + cycle_proc.alythesss_ire = get_rng( "alythesss_ire", alythesss_ire_trigger, true, [ this ]( unsigned trigger_count ) { // NOTE: 2026-03-06 Alythess's Ire usually procs at a fixed interval of attempts. Rarely, the cycle // shifts and advances the next proc; testing suggests this happens randomly in roughly ~1% of procs. return flat_rng.alythesss_ire_shift->trigger() ? rng().range( 1u, trigger_count ) : 0u; diff --git a/engine/class_modules/warlock/sc_warlock_pets.cpp b/engine/class_modules/warlock/sc_warlock_pets.cpp index a1db70a9705..a08bb797436 100644 --- a/engine/class_modules/warlock/sc_warlock_pets.cpp +++ b/engine/class_modules/warlock/sc_warlock_pets.cpp @@ -37,13 +37,16 @@ void warlock_pet_t::create_buffs() pet_t::create_buffs(); // Demonology - buffs.imp_gang_boss = make_buff( this, "imp_gang_boss", o()->talents.imp_gang_boss_buff ) + buffs.imp_gang_boss = make_buff( actor_pair_t( this, o() ), "imp_gang_boss", o()->talents.imp_gang_boss_buff ) ->set_default_value_from_effect( 2 ); - buffs.unstable_soul = make_buff( this, "unstable_soul", o()->talents.unstable_soul_buff ) + buffs.infernal_command = make_buff( actor_pair_t( this, o() ), "infernal_command", o()->warlock_base.infernal_command_buff ) + ->set_default_value( o()->warlock_base.infernal_command_buff->effectN( 1 ).percent() ); + + buffs.unstable_soul = make_buff( actor_pair_t( this, o() ), "unstable_soul", o()->talents.unstable_soul_buff ) ->set_default_value_from_effect( 1 ); - buffs.ferocity_of_fharg = make_buff( this, "ferocity_of_fharg", o()->talents.ferocity_of_fharg_buff ); + buffs.flametouched = make_buff( this, "ferocity_of_fharg", o()->talents.flametouched_buff ); buffs.demonic_power = make_buff( this, "demonic_power", o()->talents.demonic_power_buff ) ->set_default_value_from_effect( 1 ); @@ -70,12 +73,14 @@ void warlock_pet_t::create_buffs() o()->buffs.pet_movement->trigger(); else if ( cur < prev ) o()->buffs.pet_movement->decrement(); - } ); + } ) + ->set_proc_callbacks( false ); // These buffs are needed for operational purposes but serve little to no reporting purpose buffs.imp_gang_boss->quiet = true; + buffs.infernal_command->quiet = true; buffs.unstable_soul->quiet = true; - buffs.ferocity_of_fharg->quiet = true; + buffs.flametouched->quiet = true; buffs.grimoire_of_service->quiet = true; buffs.embers->quiet = true; } @@ -141,8 +146,8 @@ double warlock_pet_t::composite_melee_haste() const { double m = pet_t::composite_melee_haste(); - if ( buffs.ferocity_of_fharg->check() ) - m *= 1.0 + buffs.ferocity_of_fharg->data().effectN( 1 ).percent(); + if ( buffs.flametouched->check() ) + m *= 1.0 + buffs.flametouched->data().effectN( 1 ).percent(); return m; } @@ -151,8 +156,8 @@ double warlock_pet_t::composite_melee_auto_attack_speed() const { double m = pet_t::composite_melee_auto_attack_speed(); - if ( buffs.ferocity_of_fharg->check() ) - m /= 1.0 + buffs.ferocity_of_fharg->data().effectN( 1 ).percent(); + if ( buffs.flametouched->check() ) + m /= 1.0 + buffs.flametouched->data().effectN( 1 ).percent(); return m; } @@ -774,6 +779,10 @@ void wild_imp_pet_t::arise() 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; + // Start casting fel firebolts firebolt->set_target( o()->target ); firebolt->schedule_execute(); @@ -789,6 +798,8 @@ void wild_imp_pet_t::demise() buffs.unstable_soul->expire(); + buffs.infernal_command->expire(); + if ( o()->talents.summon_demonic_tyrant.ok() ) { for ( auto t : o()->warlock_pet_list.demonic_tyrants ) @@ -868,7 +879,7 @@ double wild_imp_pet_t::composite_player_multiplier( school_e school ) const dreadstalker_t::dreadstalker_t( warlock_t* owner ) : warlock_pet_t( owner, "dreadstalker", PET_DREADSTALKER, true ) { - npc_id = owner->talents.call_dreadstalkers_2->effectN( 1 ).misc_value1(); + npc_id = owner->talents.call_dreadstalkers_summon_1->effectN( 1 ).misc_value1(); action_list_str = "leap/travel/dreadbite"; resource_regeneration = regen_type::DISABLED; @@ -885,7 +896,7 @@ dreadstalker_t::dreadstalker_t( warlock_t* owner ) : warlock_pet_t( owner, "drea dreadstalker_t::dreadstalker_t( warlock_t* owner, util::string_view pet_name, pet_e pet_type ) : warlock_pet_t( owner, pet_name, pet_type, true ) { - npc_id = owner->talents.call_dreadstalkers_2->effectN( 1 ).misc_value1(); + npc_id = owner->talents.call_dreadstalkers_summon_1->effectN( 1 ).misc_value1(); action_list_str = "leap/travel/dreadbite"; resource_regeneration = regen_type::DISABLED; @@ -1054,7 +1065,7 @@ void dreadstalker_t::arise() } if ( o()->talents.flametouched.ok() ) - buffs.ferocity_of_fharg->trigger(); + buffs.flametouched->trigger(); dreadbite_executes = 1; @@ -1101,8 +1112,8 @@ double dreadstalker_t::composite_melee_crit_chance() const { double m = warlock_pet_t::composite_melee_crit_chance(); - if ( buffs.ferocity_of_fharg->check() ) - m += buffs.ferocity_of_fharg->data().effectN( 2 ).percent(); + if ( buffs.flametouched->check() ) + m += buffs.flametouched->data().effectN( 2 ).percent(); return m; } @@ -1111,8 +1122,8 @@ double dreadstalker_t::composite_spell_crit_chance() const { double m = warlock_pet_t::composite_spell_crit_chance(); - if ( buffs.ferocity_of_fharg->check() ) - m += buffs.ferocity_of_fharg->data().effectN( 2 ).percent(); + if ( buffs.flametouched->check() ) + m += buffs.flametouched->data().effectN( 2 ).percent(); return m; } @@ -1268,9 +1279,11 @@ void vilefiend_t::create_buffs() { warlock_simple_pet_t::create_buffs(); - mark_of_shatug = make_buff( this, "mark_of_shatug" ); + mark_of_shatug = make_buff( this, "mark_of_shatug", o()->talents.mark_of_shatug ) + ->set_proc_callbacks( false ); - mark_of_fharg = make_buff( this, "mark_of_fharg" ); + mark_of_fharg = make_buff( this, "mark_of_fharg", o()->talents.mark_of_fharg ) + ->set_proc_callbacks( false ); auto damage = new infernal_presence_t( this ); @@ -1959,9 +1972,9 @@ void infernal_t::demise() if ( o()->hero.abyssal_dominion.ok() && type == MAIN ) make_event( sim, [ this ] { - // Random extra duration time between 0_ms and 820_ms following a uniform distribution - const timespan_t dur_adjust = timespan_t::from_millis( rng().range( 0.0, 820.0 ) ); - o()->warlock_pet_list.fragments.spawn( o()->hero.infernal_fragmentation->duration() + dur_adjust, 2u ); + // Summon two infernal fragments + o()->summons.fragment->execute(); + o()->summons.fragment->execute(); } ); } @@ -2191,7 +2204,7 @@ action_t* chaos_tear_t::create_action( util::string_view name, util::string_view /// Overfiend Begin overfiend_t::overfiend_t( warlock_t* owner, util::string_view name ) - : warlock_pet_t( owner, name, PET_WARLOCK, true ) + : warlock_pet_t( owner, name, PET_WARLOCK_RANDOM, true ) { npc_id = owner->talents.summon_overfiend->effectN( 1 ).misc_value1(); @@ -2344,6 +2357,64 @@ action_t* darkglare_t::create_action( util::string_view name, util::string_view /// Darkglare End +/// Desperate Soul Begin + +desperate_soul_t::desperate_soul_t( warlock_t* owner, util::string_view name ) + : warlock_pet_t( owner, name, PET_WARLOCK_RANDOM, true ) +{ + npc_id = owner->talents.summon_desperate_soul->effectN( 1 ).misc_value1(); + + action_list_str = "wrath_of_nathreza"; +} + +struct wrath_of_nathreza_t : public warlock_pet_spell_t +{ + wrath_of_nathreza_t( warlock_pet_t* p ) + : warlock_pet_spell_t( "Wrath of Nathreza", p, p->o()->talents.wrath_of_nathreza_impact ) + { + aoe = -1; + reduced_aoe_targets = as( p->o()->talents.wrath_of_nathreza->effectN( 2 ).base_value() ); + } + + bool ready() override + { + if ( debug_cast( p() )->wraths <= 0 ) + return false; + + return warlock_pet_spell_t::ready(); + } + + void execute() override + { + if ( p()->o()->haunt_target && !p()->o()->haunt_target->is_sleeping() ) + target = p()->o()->haunt_target; + + warlock_pet_spell_t::execute(); + + debug_cast( p() )->wraths--; + + if ( debug_cast( p() )->wraths <= 0 ) + make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } ); + } +}; + +void desperate_soul_t::arise() +{ + warlock_pet_t::arise(); + + wraths = 1; +}; + +action_t* desperate_soul_t::create_action( util::string_view name, util::string_view options_str ) +{ + if ( name == "wrath_of_nathreza" ) + return new wrath_of_nathreza_t( this ); + + return warlock_pet_t::create_action( name, options_str ); +} + +/// Desperate_Soul End + } // namespace affliction namespace diabolist @@ -2663,6 +2734,9 @@ namespace diabolist warlock_pet_spell_t::execute(); debug_cast( p() )->bolts--; + + if ( debug_cast( p() )->bolts <= 0 ) + make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } ); } }; diff --git a/engine/class_modules/warlock/sc_warlock_pets.hpp b/engine/class_modules/warlock/sc_warlock_pets.hpp index de06b00debe..aea2797c46e 100644 --- a/engine/class_modules/warlock/sc_warlock_pets.hpp +++ b/engine/class_modules/warlock/sc_warlock_pets.hpp @@ -83,8 +83,9 @@ struct warlock_pet_t : public pet_t { propagate_const embers; // Infernal Shard Generation propagate_const imp_gang_boss; // Aura applied to some Wild Imps for increased damage (and size) + propagate_const infernal_command; // Aura applied to Wild Imps periodically propagate_const unstable_soul; - propagate_const ferocity_of_fharg; + propagate_const flametouched; propagate_const demonic_power; propagate_const grimoire_of_service; } buffs; @@ -425,6 +426,8 @@ struct wild_imp_pet_t : public warlock_pet_t bool is_hog_imp; bool power_siphon; bool imploded; + timespan_t infernal_command_ev_ts; + timespan_t infernal_command_ev_offset; wild_imp_pet_t( warlock_t* ); void init_base_stats() override; @@ -581,7 +584,7 @@ struct shadowy_tear_t : public warlock_pet_t int barrages; action_t* cinder; - shadowy_tear_t( warlock_t*, util::string_view = "Shadowy Tear" ); + shadowy_tear_t( warlock_t*, util::string_view = "shadowy_tear" ); void arise() override; action_t* create_action( util::string_view, util::string_view ) override; }; @@ -591,7 +594,7 @@ struct unstable_tear_t : public warlock_pet_t int barrages; action_t* cinder; - unstable_tear_t( warlock_t*, util::string_view = "Unstable Tear" ); + unstable_tear_t( warlock_t*, util::string_view = "unstable_tear" ); void arise() override; action_t* create_action( util::string_view, util::string_view ) override; }; @@ -601,14 +604,14 @@ struct chaos_tear_t : public warlock_pet_t int bolts; action_t* cinder; - chaos_tear_t( warlock_t*, util::string_view = "Chaos Tear" ); + chaos_tear_t( warlock_t*, util::string_view = "chaos_tear" ); void arise() override; action_t* create_action( util::string_view, util::string_view ) override; }; struct overfiend_t : public warlock_pet_t { - overfiend_t( warlock_t*, util::string_view = "Overfiend" ); + overfiend_t( warlock_t*, util::string_view = "overfiend" ); action_t* create_action( util::string_view, util::string_view ) override; }; } // namespace destruction @@ -622,6 +625,15 @@ struct darkglare_t : public warlock_pet_t void demise() override; action_t* create_action( util::string_view , util::string_view ) override; }; + +struct desperate_soul_t : public warlock_pet_t +{ + int wraths; + + desperate_soul_t( warlock_t*, util::string_view = "desperate_souls" ); + void arise() override; + action_t* create_action( util::string_view , util::string_view ) override; +}; } // namespace affliction namespace diabolist