Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 76 additions & 11 deletions engine/class_modules/warlock/sc_warlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 )
Expand All @@ -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<warlock_pet_t*>( pet );
Expand Down Expand Up @@ -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 );
}
}

Expand All @@ -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 )
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 ),
Expand All @@ -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 ),
Expand Down
55 changes: 46 additions & 9 deletions engine/class_modules/warlock/sc_warlock.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ struct warlock_t : public parse_player_effects_t
std::vector<event_t*> 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;

Expand All @@ -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
Expand All @@ -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
Expand All @@ -300,6 +303,7 @@ struct warlock_t : public parse_player_effects_t
spawner::pet_spawner_t<pets::destruction::infernal_t, warlock_t> infernals;

spawner::pet_spawner_t<pets::affliction::darkglare_t, warlock_t> darkglares;
spawner::pet_spawner_t<pets::affliction::desperate_soul_t, warlock_t> desperate_souls;

spawner::pet_spawner_t<pets::demonology::dreadstalker_t, warlock_t> dreadstalkers;
spawner::pet_spawner_t<pets::demonology::vilefiend_t, warlock_t> vilefiends;
Expand All @@ -313,7 +317,7 @@ struct warlock_t : public parse_player_effects_t
spawner::pet_spawner_t<pets::demonology::antoran_inquisitor_t, warlock_t> antoran_inquisitor;
spawner::pet_spawner_t<pets::demonology::antoran_jailer_t, warlock_t> antoran_jailer;

spawner::pet_spawner_t<pets::destruction::shadowy_tear_t, warlock_t> shadow_rifts;
spawner::pet_spawner_t<pets::destruction::shadowy_tear_t, warlock_t> shadowy_rifts;
spawner::pet_spawner_t<pets::destruction::unstable_tear_t, warlock_t> unstable_rifts;
spawner::pet_spawner_t<pets::destruction::chaos_tear_t, warlock_t> chaos_rifts;
spawner::pet_spawner_t<pets::destruction::infernal_roc_t, warlock_t> rocs;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 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 pet_summons_t
{
propagate_const<action_t*> lady_sacrolash;
propagate_const<action_t*> grand_warlock_alythess;
propagate_const<action_t*> antoran_inquisitor;
propagate_const<action_t*> 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 tier_sets_t
{
Expand Down Expand Up @@ -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_collection();
pet_t* create_main_pet( util::string_view pet_name, util::string_view pet_type );
std::unique_ptr<expr_t> create_pet_expression( util::string_view name_str );
};
Expand Down
Loading
Loading