From da47067bf5601b8de52dea31be0e9e03f53aba14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0efl?= Date: Wed, 26 Nov 2025 14:21:06 +0100 Subject: [PATCH 1/4] [Mage] mage.randomize_si_target option When enabled, Splitting Ice will hit a random secondary target rather than always hitting the second enemy --- engine/class_modules/sc_mage.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/engine/class_modules/sc_mage.cpp b/engine/class_modules/sc_mage.cpp index b108b0c7d99..e3bfe9f4106 100644 --- a/engine/class_modules/sc_mage.cpp +++ b/engine/class_modules/sc_mage.cpp @@ -366,6 +366,7 @@ struct mage_t final : public player_t bool fof_requires_freezing = true; bool il_requires_freezing = true; bool il_sort_by_freezing = false; + bool randomize_si_target = false; } options; // Pets @@ -1799,6 +1800,19 @@ struct mage_spell_t : public spell_t return c; } + size_t available_targets( std::vector& tl ) const override + { + spell_t::available_targets( tl ); + + if ( tl.size() > 2 && p()->options.randomize_si_target + && data().affected_by( p()->talents.splitting_ice->effectN( 1 ) ) ) + { + std::swap( tl[ 1 ], tl[ rng().range( 1, tl.size() ) ] ); + } + + return tl.size(); + } + void execute() override { spell_t::execute(); @@ -5624,6 +5638,7 @@ void mage_t::create_options() add_option( opt_bool( "mage.fof_requires_freezing", options.fof_requires_freezing ) ); add_option( opt_bool( "mage.il_requires_freezing", options.il_requires_freezing ) ); add_option( opt_bool( "mage.il_sort_by_freezing", options.il_sort_by_freezing ) ); + add_option( opt_bool( "mage.randomize_si_target", options.randomize_si_target ) ); player_t::create_options(); } From ce72e71bb100e76e880bf7459ed3b783e1a446cb Mon Sep 17 00:00:00 2001 From: Fluttershy Date: Wed, 26 Nov 2025 17:32:32 +0100 Subject: [PATCH 2/4] [Paladin] Change Wrathful Descent and Hammer of Light Cleave to secondary_targets_only (Thanks Norrinir) --- engine/class_modules/paladin/sc_paladin.cpp | 36 +++------------------ 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/engine/class_modules/paladin/sc_paladin.cpp b/engine/class_modules/paladin/sc_paladin.cpp index 163c0468492..9627ba45b4d 100644 --- a/engine/class_modules/paladin/sc_paladin.cpp +++ b/engine/class_modules/paladin/sc_paladin.cpp @@ -1864,6 +1864,7 @@ struct hammer_of_light_t : public holy_power_consumer_t affected_by.divine_purpose = false; // We handle this manually base_execute_time = timespan_t::from_millis( 600 ); // Still has a 600ms execute time, for whatever reasons. Not in spell data anymore. dual = true; + secondary_targets_only = true; if ( p->sets->has_set_bonus( HERO_TEMPLAR, TWW3, B4 ) ) // Both effect 2 and 4 adjust AoE. This is probably a tuning knob for Blizzard. Also maybe Ret is 2, Prot 4, who knows. @@ -1872,20 +1873,6 @@ struct hammer_of_light_t : public holy_power_consumer_t .base_value() ); } - size_t available_targets( std::vector& tl ) const override - { - holy_power_consumer_t::available_targets( tl ); - - // Does not hit the main target - auto it = range::find( tl, target ); - if ( it != tl.end() ) - { - tl.erase( it ); - } - - return tl.size(); - } - action_state_t* new_state() override { return new state_t( this, target ); @@ -2070,9 +2057,10 @@ struct empyrean_hammer_wd_t : public paladin_spell_t empyrean_hammer_wd_t( paladin_t* p ) : paladin_spell_t( "empyrean_hammer_wrathful_descent", p, p->spells.templar.empyrean_hammer_wd ) { - background = true; - may_crit = false; - aoe = -1; + background = true; + may_crit = false; + aoe = -1; + secondary_targets_only = true; // ToDo (Fluttershy) // This spell currently deals full damage to all targets, even above 20. @@ -2080,20 +2068,6 @@ struct empyrean_hammer_wd_t : public paladin_spell_t reduced_aoe_targets = -1; } - size_t available_targets( std::vector& tl ) const override - { - paladin_spell_t::available_targets( tl ); - - // Does not hit the main target - auto it = range::find( tl, target ); - if ( it != tl.end() ) - { - tl.erase( it ); - } - - return tl.size(); - } - void impact(action_state_t* s) override { paladin_spell_t::impact( s ); From 6d6b84825524784281925d8145d1b63b30665fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0efl?= Date: Wed, 26 Nov 2025 17:39:36 +0100 Subject: [PATCH 3/4] [Action] Support for arbitrary target filtering with target_filter_callback --- engine/action/action.cpp | 7 ++++--- engine/action/action.hpp | 10 ++++++++-- engine/class_modules/sc_mage.cpp | 12 ++++++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index 2e37c66c37b..f9845add958 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -345,7 +345,6 @@ action_t::action_t( action_e ty, util::string_view token, player_t* p, const spe internal_id( p->get_action_id( name_str ) ), resource_current( RESOURCE_NONE ), aoe(), - secondary_targets_only(), dual(), callbacks( true ), caster_callbacks( true ), @@ -1703,12 +1702,14 @@ int action_t::num_targets() const size_t action_t::available_targets( std::vector& tl ) const { tl.clear(); - if ( !secondary_targets_only && !target->is_sleeping() && target->is_enemy() ) + + const auto& cb = target_filter_callback; + if ( !target->is_sleeping() && target->is_enemy() && ( !cb || cb( this, target ) ) ) tl.push_back( target ); for ( auto* t : sim->target_non_sleeping_list ) { - if ( t->is_enemy() && ( t != target ) ) + if ( t->is_enemy() && ( t != target ) && ( !cb || cb( this, t ) ) ) { tl.push_back( t ); } diff --git a/engine/action/action.hpp b/engine/action/action.hpp index 5aa0edad98e..95a80419626 100644 --- a/engine/action/action.hpp +++ b/engine/action/action.hpp @@ -22,6 +22,7 @@ #include #include +struct action_t; struct action_priority_t; struct action_priority_list_t; struct action_state_t; @@ -51,6 +52,8 @@ namespace report { template struct parsed_value_t; +using target_filter_callback_t = std::function; + // Action =================================================================== struct action_t : private noncopyable @@ -104,8 +107,8 @@ struct action_t : private noncopyable /// The amount of targets that an ability impacts on. -1 will hit all targets. int aoe; - /// Whether the action hits all targets or only the secondary ones. - bool secondary_targets_only; + /// Which targets to include in the list of available targets. + target_filter_callback_t target_filter_callback; /// If set to true, this action will not be counted toward total amount of executes in reporting. Useful for abilities with parent/children attacks. bool dual; @@ -1143,6 +1146,9 @@ struct action_t : private noncopyable static bool has_direct_damage_effect( const spell_data_t& ); static bool has_periodic_damage_effect( const spell_data_t& ); + static target_filter_callback_t secondary_targets_only() + { return [] ( const action_t* a, player_t* t ) { return t != a->target; }; } + friend void sc_format_to( const action_t&, fmt::format_context::iterator ); }; diff --git a/engine/class_modules/sc_mage.cpp b/engine/class_modules/sc_mage.cpp index e3bfe9f4106..3d54ad574a0 100644 --- a/engine/class_modules/sc_mage.cpp +++ b/engine/class_modules/sc_mage.cpp @@ -4636,7 +4636,8 @@ struct splintering_ray_t final : public spell_t spell_t( n, p, p->find_spell( 418735 ) ), freezing_source( p->get_proc( "Freezing applied (Splintering Ray)" ) ) { - background = proc = secondary_targets_only = true; + background = proc = true; + target_filter_callback = secondary_targets_only(); base_dd_min = base_dd_max = 1.0; // TODO: Seems to hit 1 fewer target aoe--; @@ -4935,7 +4936,8 @@ struct frostfire_empowerment_t final : public spell_t spell_t( n, p, p->find_spell( 431186 ) ), freezing_source( p->get_proc( "Freezing applied (Frostfire Empowerment)" ) ) { - background = proc = secondary_targets_only = true; + background = proc = true; + target_filter_callback = secondary_targets_only(); aoe = -1; base_dd_min = base_dd_max = 1.0; // TODO: Check how it behaves wrt the excluded main target @@ -4960,7 +4962,8 @@ struct flash_freezeburn_t final : public spell_t spell_t( n, p, p->find_spell( 1278079 ) ), freezing_source( p->get_proc( "Freezing applied (Flash Freezeburn)" ) ) { - background = proc = secondary_targets_only = true; + background = proc = true; + target_filter_callback = secondary_targets_only(); base_dd_min = base_dd_max = 1.0; // TODO: Usually hits one fewer target // It's possible it picks 5 random targets and if one of them happens to be @@ -4982,7 +4985,8 @@ struct controlled_instincts_t final : public spell_t controlled_instincts_t( std::string_view n, mage_t* p ) : spell_t( n, p, p->find_spell( p->specialization() == MAGE_FROST ? 444487 : 444720 ) ) { - background = proc = secondary_targets_only = true; + background = proc = true; + target_filter_callback = secondary_targets_only(); // Only hits 5 targets despite max_targets being 6 aoe -= 1; // TODO: The tooltip still mentions this, but it's untestable at the moment since it can't hit 6 or more targets From 090cd255f9da6c00e65bf513009206b50fa9bc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0efl?= Date: Wed, 26 Nov 2025 17:41:24 +0100 Subject: [PATCH 4/4] [Paladin] Fix compilation --- engine/class_modules/paladin/sc_paladin.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/class_modules/paladin/sc_paladin.cpp b/engine/class_modules/paladin/sc_paladin.cpp index 9627ba45b4d..1b17a5df200 100644 --- a/engine/class_modules/paladin/sc_paladin.cpp +++ b/engine/class_modules/paladin/sc_paladin.cpp @@ -1864,7 +1864,7 @@ struct hammer_of_light_t : public holy_power_consumer_t affected_by.divine_purpose = false; // We handle this manually base_execute_time = timespan_t::from_millis( 600 ); // Still has a 600ms execute time, for whatever reasons. Not in spell data anymore. dual = true; - secondary_targets_only = true; + target_filter_callback = secondary_targets_only(); if ( p->sets->has_set_bonus( HERO_TEMPLAR, TWW3, B4 ) ) // Both effect 2 and 4 adjust AoE. This is probably a tuning knob for Blizzard. Also maybe Ret is 2, Prot 4, who knows. @@ -2060,7 +2060,7 @@ struct empyrean_hammer_wd_t : public paladin_spell_t background = true; may_crit = false; aoe = -1; - secondary_targets_only = true; + target_filter_callback = secondary_targets_only(); // ToDo (Fluttershy) // This spell currently deals full damage to all targets, even above 20.