Skip to content
Merged
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
6 changes: 3 additions & 3 deletions engine/class_modules/warlock/sc_warlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down
10 changes: 8 additions & 2 deletions engine/class_modules/warlock/sc_warlock.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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" };

Expand Down Expand Up @@ -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 );
Expand Down
32 changes: 13 additions & 19 deletions engine/class_modules/warlock/sc_warlock_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>( 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<int>( rng_settings.demonic_knowledge_rank2_cards.setting_value );
else if ( talents.demonic_knowledge.rank() == 1 )
cards = static_cast<int>( rng_settings.demonic_knowledge_rank1_cards.setting_value );
}
else
{
cards = static_cast<int>( 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<int>( rng_settings.demonic_knowledge_rank2_cards.setting_value );
else if ( talents.demonic_knowledge.rank() == 1 )
cards = static_cast<int>( 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 );
}
}

Expand All @@ -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<int>( rng_settings.rain_of_chaos_deck_size.setting_value );
int cards = static_cast<int>( 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.
Expand Down
112 changes: 89 additions & 23 deletions engine/class_modules/warlock/sc_warlock_pets.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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--; } );
Expand Down Expand Up @@ -206,18 +207,18 @@ 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" );
}
}

void warlock_pet_t::demise()
{
if ( !current.sleeping )
{
if ( triggers.hellbent_commander )
if ( triggers.hellbent_commander_demise )
{
o()->buffs.hellbent_commander->decrement();
}
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 );
}

Expand Down Expand Up @@ -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 );
}

Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<overlord_t*>( 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<overlord_t*>( player )->charge_executes--;
}
};

struct wicked_cleave_t : public warlock_pet_spell_t
{
wicked_cleave_t( warlock_pet_t* p )
Expand All @@ -2456,6 +2496,9 @@ namespace diabolist
warlock_pet_spell_t::execute();

debug_cast<overlord_t*>( p() )->cleaves--;

if ( debug_cast<overlord_t*>( 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?)
Expand Down Expand Up @@ -2494,13 +2537,16 @@ namespace diabolist
{
warlock_pet_t::arise();

charge_executes = 1;
cleaves = 1;
}

action_t* overlord_t::create_action( util::string_view name, util::string_view options_str )
{
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 );
}
Expand All @@ -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";
}
Expand Down Expand Up @@ -2576,6 +2623,14 @@ namespace diabolist

debug_cast<mother_of_chaos_t*>( p() )->salvos--;
}

void last_tick( dot_t* d ) override
{
warlock_pet_spell_t::last_tick( d );

if ( debug_cast<mother_of_chaos_t*>( p() )->salvos <= 0 )
make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } );
}
};

void mother_of_chaos_t::arise()
Expand All @@ -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";
Expand Down Expand Up @@ -2663,6 +2719,14 @@ namespace diabolist

debug_cast<pit_lord_t*>( p() )->felseekers--;
}

void last_tick( dot_t* d ) override
{
warlock_pet_spell_t::last_tick( d );

if ( debug_cast<pit_lord_t*>( p() )->felseekers <= 0 )
make_event( sim, 0_ms, [ this ]() { player->cast_pet()->dismiss(); } );
}
};

void pit_lord_t::arise()
Expand Down Expand Up @@ -2803,15 +2867,17 @@ 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();

resource_regeneration = regen_type::DISABLED;
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()
Expand Down
Loading
Loading