From cae2081483b861796cb5acae1fd2a8a437a6126c Mon Sep 17 00:00:00 2001 From: Justin Driggers Date: Thu, 12 Feb 2026 23:07:17 -0500 Subject: [PATCH] [Monk] Balanced Stratagem, Counterstrike * Exploding Keg travel time is not represented by missile_speed() * Initial Exploding Keg damage is not affected by Balanced Stratagem, but is consumed on cast and affects the following proc hits * Simplify Rushing Jade Wind * Snapshot Balanced Stratagem for BoF, SCK, RJW, CJL, EK * Snapshot Counterstrike for SCK * Jade Flash is not affected by Balanced Stratagem * Defer Balanced Stratagem consumption for Chi Burst until the damage and heal have executed --- engine/action/parse_effects.cpp | 4 +- engine/action/parse_effects.hpp | 12 ++ engine/class_modules/monk/sc_monk.cpp | 273 ++++++++++++++++---------- engine/class_modules/monk/sc_monk.hpp | 20 +- 4 files changed, 193 insertions(+), 116 deletions(-) diff --git a/engine/action/parse_effects.cpp b/engine/action/parse_effects.cpp index 20bf34ed119..aef1b2fbb03 100644 --- a/engine/action/parse_effects.cpp +++ b/engine/action/parse_effects.cpp @@ -1888,6 +1888,7 @@ void parse_action_base_t::parsed_effects_html( report::sc_html_stream& os ) cons }; using VEC = parse_action_base_t; + print_parsed_type( os, &VEC::persistent_multiplier_effects, "Persistent Multiplier" ); print_parsed_type( os, &VEC::da_multiplier_effects, "Direct Damage" ); print_parsed_type( os, &VEC::ta_multiplier_effects, "Periodic Damage" ); print_parsed_type( os, &VEC::crit_chance_effects, "Critical Strike Chance" ); @@ -1916,7 +1917,8 @@ void parse_action_base_t::parsed_effects_html( report::sc_html_stream& os ) cons size_t parse_action_base_t::total_effects_count() const { - return ta_multiplier_effects.size() + + return persistent_multiplier_effects.size() + + ta_multiplier_effects.size() + da_multiplier_effects.size() + execute_time_effects.size() + flat_execute_time_effects.size() + diff --git a/engine/action/parse_effects.hpp b/engine/action/parse_effects.hpp index 91645f5ef11..7c6e4ab5645 100644 --- a/engine/action/parse_effects.hpp +++ b/engine/action/parse_effects.hpp @@ -801,6 +801,7 @@ struct parse_player_effects_t : public player_t, public parse_effects_t struct parse_action_base_t : public parse_effects_t { + std::vector persistent_multiplier_effects; std::vector ta_multiplier_effects; std::vector da_multiplier_effects; std::vector execute_time_effects; @@ -971,6 +972,7 @@ struct parse_action_effects_t : public BASE, public parse_action_base_t // the final derived constructor. if ( !BASE::does_direct_damage() && !BASE::does_periodic_damage() ) { + remove_damage_entries( persistent_multiplier_effects, "persistent_multiplier_effects" ); remove_damage_entries( ta_multiplier_effects, "tick damage" ); remove_damage_entries( da_multiplier_effects, "direct damage" ); remove_damage_entries( crit_bonus_effects, "crit bonus multiplier" ); @@ -1045,6 +1047,16 @@ struct parse_action_effects_t : public BASE, public parse_action_base_t return da; } + double composite_persistent_multiplier( const action_state_t* s ) const override + { + auto p = BASE::composite_persistent_multiplier( s ); + + for ( const auto& i : persistent_multiplier_effects ) + p *= 1.0 + get_effect_value( i, true ); + + return p; + } + double composite_crit_chance() const override { auto cc = BASE::composite_crit_chance(); diff --git a/engine/class_modules/monk/sc_monk.cpp b/engine/class_modules/monk/sc_monk.cpp index 5097e27b69b..3ad3c3133c1 100644 --- a/engine/class_modules/monk/sc_monk.cpp +++ b/engine/class_modules/monk/sc_monk.cpp @@ -128,10 +128,7 @@ void monk_action_t::apply_buff_effects() // Brewmaster parse_effects( p()->buff.blackout_combo ); parse_effects( p()->buff.celestial_flames ); - parse_effects( - p()->buff.counterstrike, - affect_list_t( 1 ).add_spell( p()->baseline.brewmaster.spinning_crane_kick->effectN( 1 ).trigger()->id() ), - CONSUME_BUFF ); + parse_effects( p()->buff.counterstrike, CONSUME_BUFF ); parse_effects( p()->buff.empty_barrel ); // Windwalker @@ -178,7 +175,11 @@ void monk_action_t::apply_buff_effects() parse_effects( p()->talent.master_of_harmony.aspect_of_harmony_heal, [ & ] { return p()->buff.aspect_of_harmony.heal_ticking(); } ); parse_effects( p()->buff.balanced_stratagem_physical, CONSUME_BUFF ); - parse_effects( p()->buff.balanced_stratagem_magic, CONSUME_BUFF ); + parse_effects( p()->buff.balanced_stratagem_magic, + affect_list_t( 1 ) + .remove_spell( p()->baseline.monk.crackling_jade_lightning->id() ) + .remove_spell( p()->talent.brewmaster.exploding_keg->id() ), + CONSUME_BUFF ); // Shado-Pan @@ -1330,20 +1331,44 @@ struct blackout_kick_t : overwhelming_force_ttalent.brewmaster.rushing_jade_wind_buff->effectN( 1 ).base_value(); + + if ( const auto &effect = player->talent.master_of_harmony.balanced_stratagem_physical->effectN( 1 ); + effect.ok() ) + add_parse_entry( persistent_multiplier_effects ) + .set_buff( player->buff.balanced_stratagem_physical ) + .set_value( effect.percent() ) + .set_eff( &effect ) + .add_parse_callback( this, PARSE_CALLBACK_POST_EXECUTE, + [ this, b = player->buff.balanced_stratagem_physical.get() ]( action_state_t * ) { + b->consume( this ); + } ); + } + }; rushing_jade_wind_t( monk_t *player, std::string_view options_str ) - : monk_melee_attack_t( player, "rushing_jade_wind", player->talent.brewmaster.rushing_jade_wind ), - buff( player->buff.rushing_jade_wind ) + : monk_melee_attack_t( player, "rushing_jade_wind", player->talent.brewmaster.rushing_jade_wind ) { parse_options( options_str ); + + tick_action = new tick_t( player, "rushing_jade_wind_tick", player->talent.brewmaster.rushing_jade_wind_tick ); + add_child( tick_action ); + + cast_during_sck = true; } void execute() override { monk_melee_attack_t::execute(); - buff->trigger(); + p()->buff.rushing_jade_wind->trigger(); } }; @@ -1359,6 +1384,26 @@ struct spinning_crane_kick_t : public monk_melee_attack_t reduced_aoe_targets = player->baseline.monk.spinning_crane_kick->effectN( 1 ).base_value(); ww_mastery = true; ap_type = attack_power_type::WEAPON_BOTH; + + if ( const auto &effect = player->talent.brewmaster.counterstrike->effectN( 1 ); effect.ok() ) + add_parse_entry( persistent_multiplier_effects ) + .set_buff( player->buff.counterstrike ) + .set_value( effect.percent() ) + .set_eff( &effect ) + .add_parse_callback( + this, PARSE_CALLBACK_POST_EXECUTE, + [ this, b = player->buff.counterstrike.get() ]( action_state_t * ) { b->consume( this ); } ); + + if ( const auto &effect = player->talent.master_of_harmony.balanced_stratagem_physical->effectN( 1 ); + effect.ok() ) + add_parse_entry( persistent_multiplier_effects ) + .set_buff( player->buff.balanced_stratagem_physical ) + .set_value( effect.percent() ) + .set_eff( &effect ) + .add_parse_callback( this, PARSE_CALLBACK_POST_EXECUTE, + [ this, b = player->buff.balanced_stratagem_physical.get() ]( action_state_t * ) { + b->consume( this ); + } ); } result_amount_type report_amount_type( const action_state_t * ) const override @@ -2724,6 +2769,12 @@ struct chi_burst_t : monk_spell_t for ( const auto &effect : spell_data->effects() ) if ( effect.type() == E_SCHOOL_DAMAGE ) TBase::ww_mastery = true; + + if ( const auto &effect = player->talent.master_of_harmony.balanced_stratagem_magic->effectN( 1 ); effect.ok() ) + add_parse_entry( da_multiplier_effects ) + .set_buff( player->buff.balanced_stratagem_magic ) + .set_value( effect.percent() ) + .set_eff( &effect ); } }; @@ -2758,7 +2809,6 @@ struct chi_burst_t : monk_spell_t void execute() override { p()->buff.aspect_of_harmony.trigger_path_of_resurgence(); - monk_spell_t::execute(); if ( buff ) { @@ -2770,6 +2820,9 @@ struct chi_burst_t : monk_spell_t damage->execute(); heal->execute(); + + // Defer consumption of buffs until after the damage and heal are executed + monk_spell_t::execute(); } }; @@ -2906,6 +2959,15 @@ struct crackling_jade_lightning_t : public monk_spell_t aoe_dot = new aoe_dot_t( player ); add_child( aoe_dot ); } + + if ( const auto &effect = player->talent.master_of_harmony.balanced_stratagem_magic->effectN( 1 ); effect.ok() ) + add_parse_entry( persistent_multiplier_effects ) + .set_buff( player->buff.balanced_stratagem_magic ) + .set_value( effect.percent() ) + .set_eff( &effect ) + .add_parse_callback( + this, PARSE_CALLBACK_POST_EXECUTE, + [ this, b = player->buff.balanced_stratagem_magic.get() ]( action_state_t * ) { b->consume( this ); } ); } void execute() override @@ -2960,11 +3022,18 @@ struct breath_of_fire_t : public monk_spell_t { struct dot_t : public monk_spell_t { - dot_t( monk_t *p ) : monk_spell_t( p, "breath_of_fire_dot", p->talent.brewmaster.breath_of_fire_dot ) + dot_t( monk_t *player ) : monk_spell_t( player, "breath_of_fire_dot", player->talent.brewmaster.breath_of_fire_dot ) { background = true; tick_may_crit = may_crit = true; hasted_ticks = false; + + // Balanced Stratagem is consumed by the parent action, but still present when the dot is executed + if ( const auto &effect = player->talent.master_of_harmony.balanced_stratagem_magic->effectN( 1 ); effect.ok() ) + add_parse_entry( persistent_multiplier_effects ) + .set_buff( player->buff.balanced_stratagem_magic ) + .set_value( effect.percent() ) + .set_eff( &effect ); } }; @@ -3077,11 +3146,20 @@ struct fortifying_brew_t : brew_t struct exploding_keg_proc_t : public monk_spell_t { - exploding_keg_proc_t( monk_t *p ) - : monk_spell_t( p, "exploding_keg_proc", p->talent.brewmaster.exploding_keg->effectN( 4 ).trigger() ) + exploding_keg_proc_t( monk_t *player ) + : monk_spell_t( player, "exploding_keg_proc", player->talent.brewmaster.exploding_keg->effectN( 4 ).trigger() ) { background = dual = true; proc = true; + + if ( const auto &effect = player->talent.master_of_harmony.balanced_stratagem_magic->effectN( 1 ); effect.ok() ) + add_parse_entry( da_multiplier_effects ) + .set_value( effect.percent() ) + .set_value_func( [ & ]( double base ) { + // Balanced Stratagem stacks are captured as the value on the Exploding Keg buff when it's triggered. + return base * p()->buff.exploding_keg->check_value(); + } ) + .set_eff( &effect ); } }; @@ -3096,15 +3174,12 @@ struct exploding_keg_t : public monk_spell_t add_child( p->action.exploding_keg ); } - timespan_t travel_time() const override - { - // Always has the same time to land regardless of distance, probably represented there. - return timespan_t::from_seconds( data().missile_speed() ); - } - void execute() override { - p()->buff.exploding_keg->trigger(); + // The initial EK hit is not affected by Balanced Stratagem, so we consume the stacks here. + // The stacks are captured as the value on the buff for exploding_keg_proc_t to use later. + p()->buff.exploding_keg->trigger( 1, p()->buff.balanced_stratagem_magic->consume( this ) ); + p()->buff.empty_the_cellar->trigger(); if ( p()->talent.brewmaster.fuel_on_the_fire->ok() ) @@ -4192,67 +4267,6 @@ struct zenith_t : monk_buff_t<> } }; -struct rushing_jade_wind_buff_t : public monk_buff_t<> -{ - struct tick_action_t : actions::monk_melee_attack_t - { - tick_action_t( monk_t *p ) - : monk_melee_attack_t( p, "rushing_jade_wind_tick", p->talent.shared_spell.rushing_jade_wind_tick ) - { - ww_mastery = true; - - dual = background = true; - aoe = -1; - reduced_aoe_targets = p->talent.shared_spell.rushing_jade_wind_buff->effectN( 1 ).base_value(); - - // Merge action statistics if RJW exists as an active ability - if ( const action_t *action = p->find_action( "rushing_jade_wind" ); action ) - stats = action->stats; - } - }; - - timespan_t _period; - action_t *rushing_jade_wind_tick; - - rushing_jade_wind_buff_t( monk_t *player ) - : monk_buff_t( player, "rushing_jade_wind", player->talent.shared_spell.rushing_jade_wind_buff ), - rushing_jade_wind_tick( nullptr ) - { - set_tick_time_behavior( buff_tick_time_behavior::CUSTOM ); - set_tick_time_callback( [ this ]( const buff_t *, unsigned int ) { return _period; } ); - - set_tick_callback( [ this ]( buff_t *, int, timespan_t ) { - if ( rushing_jade_wind_tick ) - { - rushing_jade_wind_tick->execute(); - return; - } - - if ( action_t *rjw = p().find_action( "rushing_jade_wind_tick" ); rjw ) - { - rushing_jade_wind_tick = rjw; - rjw->execute(); - } - } ); - set_tick_behavior( buff_tick_behavior::REFRESH ); - set_refresh_behavior( buff_refresh_behavior::PANDEMIC ); - } - - bool trigger( int stacks, double value, double chance, timespan_t duration ) override - { - // RJW snapshots the tick period on cast. - if ( duration == timespan_t::min() ) - { - duration = monk_buff_t::buff_duration(); - duration *= p().cache.spell_cast_speed(); - } - - _period = monk_buff_t::buff_period * p().cache.spell_cast_speed(); - - return monk_buff_t::trigger( stacks, value, chance, duration ); - } -}; - struct invoke_xuen_the_white_tiger_buff_t : public monk_buff_t<> { double multiplier; @@ -4660,6 +4674,51 @@ aspect_of_harmony_t::spender_t::tick_t::tick_t( monk_t *player, s { } +balanced_stratagem_t::balanced_stratagem_t( monk_t *player, std::string_view name, const spell_data_t *spell_data, + std::unordered_set allowlist ) + : monk_buff_t<>( player, fmt::format( "balanced_stratagem_{}", name ), spell_data ), + allowlist( std::move( allowlist ) ) +{ + // Remove IDs that weren't found so they don't unintentionally trigger on auto attacks + this->allowlist.erase( 0 ); +} + +bool balanced_stratagem_t::trigger( const action_state_t *state ) +{ + if ( is_fallback ) + return false; + + if ( range::contains( allowlist, state->action->id ) ) + return monk_buff_t::trigger(); + + return false; +} + +struct balanced_stratagem_magic_t : balanced_stratagem_t +{ + balanced_stratagem_magic_t( monk_t *player ) + : balanced_stratagem_t( + player, "magic", player->talent.master_of_harmony.balanced_stratagem_magic, + { player->baseline.monk.blackout_kick->id(), player->baseline.brewmaster.blackout_kick->id(), + player->talent.brewmaster.keg_smash->id(), player->talent.brewmaster.rushing_jade_wind->id(), + player->baseline.monk.spinning_crane_kick->id(), player->baseline.monk.tiger_palm->id(), + player->talent.brewmaster.press_the_advantage_tiger_palm->id() } ) + { + } +}; + +struct balanced_stratagem_physical_t : balanced_stratagem_t +{ + balanced_stratagem_physical_t( monk_t *player ) + : balanced_stratagem_t( player, "physical", player->talent.master_of_harmony.balanced_stratagem_physical, + { player->talent.brewmaster.breath_of_fire->id(), player->talent.monk.chi_burst->id(), + player->baseline.monk.crackling_jade_lightning->id(), + player->baseline.monk.expel_harm->id(), player->talent.brewmaster.exploding_keg->id(), + player->talent.monk.soothing_mist->id(), player->baseline.monk.vivify->id() } ) + { + } +}; + fractional_absorb_t::fractional_absorb_t( monk_t *player, std::string_view name, const spell_data_t *spell_data ) : monk_buff_t( player, name, spell_data ), absorb_fraction( 1.0 ) { @@ -5305,6 +5364,8 @@ void monk_t::init_spells() talent.brewmaster.special_delivery = _ST( "Special Delivery" ); talent.brewmaster.special_delivery_missile = find_spell( 196732 ); talent.brewmaster.rushing_jade_wind = _ST( "Rushing Jade Wind" ); + talent.brewmaster.rushing_jade_wind_buff = find_spell( 116847 ); + talent.brewmaster.rushing_jade_wind_tick = find_spell( 148187 ); talent.brewmaster.spirit_of_the_ox = _ST( "Spirit of the Ox" ); talent.brewmaster.jade_flash = _ST( "Jade Flash" ); talent.brewmaster.celestial_brew = _ST( "Celestial Brew" ); @@ -5568,10 +5629,6 @@ void monk_t::init_spells() tier.mid1.brm_4pc_extra_kick = find_spell( 1272464 ); } - // Shared Talent Spells - talent.shared_spell.rushing_jade_wind_buff = find_spell( 116847 ); - talent.shared_spell.rushing_jade_wind_tick = find_spell( 148187 ); - // Register passives // Instant Spells with a reduced GCD register_passive_affect_list( baseline.brewmaster.aura_2, affect_list_t( 3 ).remove_label( 640 ) ); @@ -5635,10 +5692,6 @@ void monk_t::init_background_actions() using namespace actions; base_t::init_background_actions(); - // we just look it up via `find_action` anyway, so it doesn't need to explicitly - // be set anywhere (for now) - new buffs::rushing_jade_wind_buff_t::tick_action_t( this ); - // General action.chi_wave = new chi_wave_t( this ); @@ -5814,8 +5867,8 @@ void monk_t::create_buffs() buff.fortifying_brew = make_buff_fallback( talent.monk.fortifying_brew->ok() && specialization() == MONK_BREWMASTER, this, "fortifying_brew" ); - buff.rushing_jade_wind = make_buff_fallback( - talent.brewmaster.rushing_jade_wind->ok(), this, "rushing_jade_wind" ); + buff.rushing_jade_wind = make_buff_fallback( talent.brewmaster.rushing_jade_wind->ok(), this, "rushing_jade_wind", + talent.brewmaster.rushing_jade_wind_buff ); buff.spinning_crane_kick = make_buff( this, "spinning_crane_kick", baseline.monk.spinning_crane_kick ) ->set_default_value_from_effect( 2 ) @@ -5855,8 +5908,7 @@ void monk_t::create_buffs() talent.brewmaster.empty_the_cellar_buff ); buff.exploding_keg = make_buff_fallback( talent.brewmaster.exploding_keg->ok(), this, "exploding_keg", - talent.brewmaster.exploding_keg ) - ->set_default_value_from_effect( 2 ); + talent.brewmaster.exploding_keg ); if ( talent.brewmaster.gift_of_the_ox->ok() || talent.brewmaster.spirit_of_the_ox->ok() ) buff.gift_of_the_ox = new buffs::gift_of_the_ox_t( this ); @@ -6075,12 +6127,10 @@ void monk_t::create_buffs() buff.aspect_of_harmony.construct_buffs( this ); - buff.balanced_stratagem_magic = - make_buff_fallback( talent.master_of_harmony.balanced_stratagem->ok(), this, "balanced_stratagem_magic", - talent.master_of_harmony.balanced_stratagem_magic ); - buff.balanced_stratagem_physical = - make_buff_fallback( talent.master_of_harmony.balanced_stratagem->ok(), this, "balanced_stratagem_physical", - talent.master_of_harmony.balanced_stratagem_physical ); + buff.balanced_stratagem_magic = make_buff_fallback( + talent.master_of_harmony.balanced_stratagem->ok(), this, "balanced_stratagem_magic" ); + buff.balanced_stratagem_physical = make_buff_fallback( + talent.master_of_harmony.balanced_stratagem->ok(), this, "balanced_stratagem_physical" ); // Master of Harmony buff.harmonic_surge = make_buff_fallback( talent.master_of_harmony.harmonic_surge->ok(), this, "harmonic_surge", @@ -6370,16 +6420,21 @@ void monk_t::init_special_effects() } ); if ( talent.master_of_harmony.balanced_stratagem->ok() ) - create_proc_callback( { talent.master_of_harmony.balanced_stratagem.spell() } ) - ->register_callback_trigger_function( dbc_proc_callback_t::trigger_fn_type::CONDITION, - [ & ]( const dbc_proc_callback_t *, action_t *, action_state_t *state ) { - return state->action->school != SCHOOL_NONE; - } ) - ->register_callback_execute_function( [ & ]( const dbc_proc_callback_t *, action_t *, action_state_t *state ) { - if ( state->action->school == SCHOOL_PHYSICAL ) - buff.balanced_stratagem_magic->trigger(); - if ( state->action->school != SCHOOL_PHYSICAL ) - buff.balanced_stratagem_physical->trigger(); + create_proc_callback( { talent.master_of_harmony.balanced_stratagem, + static_cast( PF_ALL_DAMAGE | PF_ALL_HEAL | PF_CAST_SUCCESSFUL ), + static_cast( PF2_ALL_CAST | PF2_ALL_HIT ) } ) + ->register_callback_trigger_function( + dbc_proc_callback_t::trigger_fn_type::CONDITION, + [ & ]( const dbc_proc_callback_t *, action_t *, action_state_t *state ) { + return debug_cast( buff.balanced_stratagem_magic.get() ) + ->trigger( state ) || + debug_cast( buff.balanced_stratagem_physical.get() ) + ->trigger( state ); + } ) + ->register_post_init_callback( []( monk_effect_callback_t *cb ) { + cb->proc_chance = 1.0; + cb->can_proc_from_procs = true; + cb->can_only_proc_from_class_abilites = true; } ); if ( talent.conduit_of_the_celestials.courage_of_the_white_tiger->ok() ) diff --git a/engine/class_modules/monk/sc_monk.hpp b/engine/class_modules/monk/sc_monk.hpp index a6ca87afeb6..f07888e11b4 100644 --- a/engine/class_modules/monk/sc_monk.hpp +++ b/engine/class_modules/monk/sc_monk.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "simulationcraft.hpp" @@ -334,6 +335,17 @@ struct aspect_of_harmony_t bool heal_ticking(); }; +struct balanced_stratagem_t : monk_buff_t<> +{ + std::unordered_set allowlist; + + balanced_stratagem_t( monk_t *player, std::string_view name, const spell_data_t *spell_data, + std::unordered_set allowlist ); + + using monk_buff_t<>::trigger; + bool trigger( const action_state_t * ); +}; + struct fractional_absorb_t : public monk_buff_t { double absorb_fraction; @@ -673,12 +685,6 @@ struct monk_t : public stagger_t struct { - struct - { - const spell_data_t *rushing_jade_wind_buff; - const spell_data_t *rushing_jade_wind_tick; - } shared_spell; - struct { // Row 1 @@ -790,6 +796,8 @@ struct monk_t : public stagger_t player_talent_t special_delivery; const spell_data_t *special_delivery_missile; player_talent_t rushing_jade_wind; + const spell_data_t *rushing_jade_wind_buff; + const spell_data_t *rushing_jade_wind_tick; player_talent_t spirit_of_the_ox; // row 5 player_talent_t jade_flash;