From 4e489915d81d72f238a036052de299dd3ee8e931 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Fri, 22 May 2026 07:28:02 -0700 Subject: [PATCH 1/4] [Buff] allow passing value argument to stat_buff_t::bump --- engine/buff/buff.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/buff/buff.cpp b/engine/buff/buff.cpp index 4cfb884933b..2d17e4b39b4 100644 --- a/engine/buff/buff.cpp +++ b/engine/buff/buff.cpp @@ -3308,9 +3308,9 @@ double stat_buff_t::buff_stat_stack_amount( const buff_stat_t& buff_stat, int s return buff_stat.stack_amount( s ); } -void stat_buff_t::bump( int stacks, double /* value */ ) +void stat_buff_t::bump( int stacks, double value ) { - buff_t::bump( stacks ); + buff_t::bump( stacks, value ); for ( auto& buff_stat : stats ) { From 2fe7b0489bf3839457f60f029af74b0f2f400965 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Fri, 22 May 2026 11:23:27 -0700 Subject: [PATCH 2/4] [Buff] minor refactor & rounding fix to stat_buff_t stat adjustment * apply truncation at the final step before applying to player stats, after any adjustments via overrides to buff_stat_tack_amount() * centralize player stat adjustment to stat_buff_t::update_player_buff_stat() --- engine/buff/buff.cpp | 50 ++++++++++++---------- engine/buff/buff.hpp | 22 ++++------ engine/class_modules/sc_evoker.cpp | 26 +---------- engine/player/azerite_data.cpp | 5 ++- engine/player/unique_gear_bfa.cpp | 13 +----- engine/player/unique_gear_thewarwithin.cpp | 6 +-- 6 files changed, 45 insertions(+), 77 deletions(-) diff --git a/engine/buff/buff.cpp b/engine/buff/buff.cpp index 2d17e4b39b4..e01a2941a48 100644 --- a/engine/buff/buff.cpp +++ b/engine/buff/buff.cpp @@ -3303,9 +3303,31 @@ stat_buff_t* stat_buff_t::set_stat_from_effect_type( effect_subtype_t type, doub return add_stat_from_effect_type( type, a, c ); } -double stat_buff_t::buff_stat_stack_amount( const buff_stat_t& buff_stat, int s ) const +double stat_buff_t::buff_stat_stack_amount( const buff_stat_t& buff_stat, int stacks ) const { - return buff_stat.stack_amount( s ); + return std::max( 1.0, std::fabs( buff_stat.amount ) ) * stacks; +} + +void stat_buff_t::update_player_buff_stat( buff_stat_t& buff_stat, int stacks ) +{ + // Blizzard likes to use effect coefficients that give (almost) exact values at the + // intended level. Small floating point conversion errors can add up to give the wrong + // value. We compensate by increasing the absolute value by a tiny bit before truncating. + double _val = buff_stat_stack_amount( buff_stat, stacks ); + _val = std::copysign( std::trunc( _val + stat_fp_epsilon ), buff_stat.amount ); + + double delta = _val - buff_stat.current_value; + + if ( delta > 0 ) + { + player->stat_gain( buff_stat.stat, delta, stat_gain, nullptr, buff_duration() > 0_ms ); + } + else if ( delta < 0 ) + { + player->stat_loss( buff_stat.stat, std::fabs( delta ), stat_gain, nullptr, buff_duration() > 0_ms ); + } + + buff_stat.current_value += delta; } void stat_buff_t::bump( int stacks, double value ) @@ -3317,17 +3339,7 @@ void stat_buff_t::bump( int stacks, double value ) if ( buff_stat.check_func && !buff_stat.check_func( *this ) ) continue; - double delta = buff_stat_stack_amount( buff_stat, current_stack ) - buff_stat.current_value; - if ( delta > 0 ) - { - player->stat_gain( buff_stat.stat, delta, stat_gain, nullptr, buff_duration() > timespan_t::zero() ); - } - else if ( delta < 0 ) - { - player->stat_loss( buff_stat.stat, std::fabs( delta ), stat_gain, nullptr, buff_duration() > timespan_t::zero() ); - } - - buff_stat.current_value += delta; + update_player_buff_stat( buff_stat, current_stack ); } } @@ -3347,17 +3359,9 @@ void stat_buff_t::decrement( int stacks, double /* value */ ) for ( auto& buff_stat : stats ) { - double delta = buff_stat.current_value - buff_stat_stack_amount( buff_stat, new_stack ); - if ( delta > 0 ) - { - player->stat_loss( buff_stat.stat, delta, stat_gain, nullptr, buff_duration() > timespan_t::zero() ); - } - else if ( delta < 0 ) - { - player->stat_gain( buff_stat.stat, std::fabs( delta ), stat_gain, nullptr, buff_duration() > timespan_t::zero() ); - } - buff_stat.current_value -= delta; + update_player_buff_stat( buff_stat, new_stack ); } + current_stack -= stacks; invalidate_cache(); diff --git a/engine/buff/buff.hpp b/engine/buff/buff.hpp index e07a8a0980c..357d1082feb 100644 --- a/engine/buff/buff.hpp +++ b/engine/buff/buff.hpp @@ -466,26 +466,17 @@ struct stat_buff_t : public buff_t double current_value; stat_check_fn check_func; - buff_stat_t( stat_e s, double a, - std::function c = std::function() ) + buff_stat_t( stat_e s, double a, std::function c = nullptr ) : stat( s ), amount( a ), current_value( 0 ), check_func( std::move( c ) ) - { - } - - double stack_amount( int stacks ) const - { - // Blizzard likes to use effect coefficients that give (almost) exact values at the - // intended level. Small floating point conversion errors can add up to give the wrong - // value. We compensate by increasing the absolute value by a tiny bit before truncating. - double val = std::max( 1.0, std::fabs( amount ) ); - return std::copysign( std::trunc( stacks * val + 1e-3 ), amount ); - } + {} }; + std::vector stats; gain_t* stat_gain; bool manual_stats_added; - virtual double buff_stat_stack_amount( const buff_stat_t&, int ) const; + virtual double buff_stat_stack_amount( const buff_stat_t&, int stacks ) const; + void update_player_buff_stat( buff_stat_t&, int stacks ); void bump ( int stacks = 1, double value = -1.0 ) override; void decrement( int stacks = 1, double value = -1.0 ) override; @@ -500,6 +491,9 @@ struct stat_buff_t : public buff_t stat_buff_t( actor_pair_t q, util::string_view name ); stat_buff_t( actor_pair_t q, util::string_view name, const spell_data_t*, const item_t* item = nullptr ); + + // floating point compensation before truncating for final amount to apply to player stats + static constexpr double stat_fp_epsilon = 1e-3; }; struct absorb_buff_t : public buff_t diff --git a/engine/class_modules/sc_evoker.cpp b/engine/class_modules/sc_evoker.cpp index 57c1b853049..c7737bd790b 100644 --- a/engine/class_modules/sc_evoker.cpp +++ b/engine/class_modules/sc_evoker.cpp @@ -4583,18 +4583,7 @@ struct ebon_might_t : public evoker_augment_t if ( buff_stat.check_func && !buff_stat.check_func( *b ) ) continue; - double delta = buff_stat.stack_amount( b->current_stack ) - buff_stat.current_value; - if ( delta > 0 ) - { - b->player->stat_gain( buff_stat.stat, delta, b->stat_gain, nullptr, b->buff_duration() > timespan_t::zero() ); - } - else if ( delta < 0 ) - { - b->player->stat_loss( buff_stat.stat, std::fabs( delta ), b->stat_gain, nullptr, - b->buff_duration() > timespan_t::zero() ); - } - - buff_stat.current_value += delta; + b->update_player_buff_stat( buff_stat, b->current_stack ); } } @@ -8132,18 +8121,7 @@ struct blistering_scales_buff_t : public evoker_buff_t if ( buff_stat.check_func && !buff_stat.check_func( *b ) ) continue; - double delta = buff_stat.stack_amount( b->current_stack ) - buff_stat.current_value; - if ( delta > 0 ) - { - b->player->stat_gain( buff_stat.stat, delta, b->stat_gain, nullptr, b->buff_duration() > timespan_t::zero() ); - } - else if ( delta < 0 ) - { - b->player->stat_loss( buff_stat.stat, std::fabs( delta ), b->stat_gain, nullptr, - b->buff_duration() > timespan_t::zero() ); - } - - buff_stat.current_value += delta; + b->update_player_buff_stat( buff_stat, b->current_stack ); } } diff --git a/engine/player/azerite_data.cpp b/engine/player/azerite_data.cpp index 4c50905c163..ee51c6c1444 100644 --- a/engine/player/azerite_data.cpp +++ b/engine/player/azerite_data.cpp @@ -5208,7 +5208,10 @@ struct worldvein_resonance_buff_t : public buff_t if ( lifeblood->check() ) { - double delta = stat_entry.stack_amount( lifeblood->current_stack ) - stat_entry.current_value; + double _val = lifeblood->buff_stat_stack_amount( stat_entry, lifeblood->current_stack ); + _val = std::copysign( std::trunc( _val + stat_buff_t::stat_fp_epsilon ), stat_entry.amount ); + + double delta = _val - stat_entry.current_value; sim->print_debug( "{} worldvein_resonance {}creases lifeblood stats by {}%," " stacks={}, old={}, new={} ({}{})", player->name(), diff --git a/engine/player/unique_gear_bfa.cpp b/engine/player/unique_gear_bfa.cpp index e1e89365221..b1dcdafe47a 100644 --- a/engine/player/unique_gear_bfa.cpp +++ b/engine/player/unique_gear_bfa.cpp @@ -4560,18 +4560,7 @@ void items::ingenious_mana_battery( special_effect_t& effect ) buff_stat.amount = value; - double delta = buff_stat_stack_amount( buff_stat, current_stack ) - buff_stat.current_value; - if ( delta > 0 ) - { - player->stat_gain( buff_stat.stat, delta, stat_gain, nullptr, buff_duration() > timespan_t::zero() ); - } - else if ( delta < 0 ) - { - player->stat_loss( buff_stat.stat, std::fabs( delta ), stat_gain, nullptr, - buff_duration() > timespan_t::zero() ); - } - - buff_stat.current_value += delta; + update_player_buff_stat( buff_stat, current_stack ); } } diff --git a/engine/player/unique_gear_thewarwithin.cpp b/engine/player/unique_gear_thewarwithin.cpp index a750a26efab..96fafa94c31 100644 --- a/engine/player/unique_gear_thewarwithin.cpp +++ b/engine/player/unique_gear_thewarwithin.cpp @@ -1872,12 +1872,12 @@ void ovinaxs_mercurial_egg( special_effect_t& effect ) {} // values can be off by a +/-2 due to unknown rounding being performed by the in-game script + // TODO: confirm truncation happens on final amount, and not per stack amount double buff_stat_stack_amount( const buff_stat_t& stat, int s ) const override { - double val = std::max( 1.0, std::fabs( stat.amount ) ); double stack = s <= cap ? s : cap + ( s - cap ) * cap_mul; - // TODO: confirm truncation happens on final amount, and not per stack amount - return std::copysign( std::trunc( stack * val + 1e-3 ), stat.amount ); + + return buff_stat_stack_amount( stat, stack ); } }; From 142cea6b3ed34c09a9aec63b2d8b5e62cdc9d6c8 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Fri, 22 May 2026 12:56:36 -0700 Subject: [PATCH 3/4] [Gear] use buff_stat_stack_amount for solarflare prism --- engine/player/unique_gear_midnight.cpp | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/engine/player/unique_gear_midnight.cpp b/engine/player/unique_gear_midnight.cpp index 9da1458db21..6e7a6a279dd 100644 --- a/engine/player/unique_gear_midnight.cpp +++ b/engine/player/unique_gear_midnight.cpp @@ -1415,24 +1415,18 @@ void solarflare_prism( special_effect_t& effect ) { double hp_inc; double max_val; - double hp_mult; solarflare_prism_buff_t( std::string_view n, const special_effect_t& e ) - : stat_buff_t( e.player, n, e.player->find_spell( 1255504 ) ), hp_inc( 0.0 ), max_val( 0.0 ), hp_mult( 0.0 ) + : stat_buff_t( e.player, n, e.player->find_spell( 1255504 ) ), + hp_inc( e.driver()->effectN( 2 ).average( e ) ), + max_val( e.driver()->effectN( 4 ).average( e ) ) { - set_default_value( e.driver()->effectN( 1 ).average( e ) ); - - hp_inc = e.driver()->effectN( 2 ).average( e ); - max_val = e.driver()->effectN( 4 ).average( e ); + add_stat_from_effect_type( A_MOD_RATING, e.driver()->effectN( 1 ).average( e ) ); } - void bump( int s, double v ) override + double buff_stat_stack_amount( const buff_stat_t& stat, int stack ) const override { - for ( auto& stat : stats ) - { - stat.amount = std::min( default_value + hp_inc * hp_mult, max_val ); - } - stat_buff_t::bump( s, v ); + return std::min( stat.amount + hp_inc * check_value(), max_val ); } }; @@ -1440,16 +1434,14 @@ void solarflare_prism( special_effect_t& effect ) { buff_t* buff; - solarflare_prism_cb_t( const special_effect_t& e ) : dbc_proc_callback_t( e.player, e ), buff( nullptr ) + solarflare_prism_cb_t( const special_effect_t& e ) : dbc_proc_callback_t( e.player, e ) { buff = make_buff( "solarflare_prism", e ); } void execute( const spell_data_t*, player_t* t, action_state_t* ) override { - solarflare_prism_buff_t* sf_buff = debug_cast( buff ); - sf_buff->hp_mult = 100 - t->health_percentage(); - sf_buff->trigger(); + buff->trigger( -1, 100.0 - t->health_percentage() ); } }; From 4ba44d538578b0c60cbed68ce89a8105a8e6f5f6 Mon Sep 17 00:00:00 2001 From: gastank <42421688+gastank@users.noreply.github.com> Date: Fri, 22 May 2026 13:40:04 -0700 Subject: [PATCH 4/4] [Gear] sporelord's mycelium initial implementation --- engine/player/unique_gear_midnight.cpp | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/engine/player/unique_gear_midnight.cpp b/engine/player/unique_gear_midnight.cpp index 6e7a6a279dd..8247186594b 100644 --- a/engine/player/unique_gear_midnight.cpp +++ b/engine/player/unique_gear_midnight.cpp @@ -3050,6 +3050,39 @@ void gloomspattered_dreadscale( special_effect_t& effect ) effect.execute_action = create_proc_action( "gloomspattered_dreadscale", effect ); } + +// 1284696 driver +// 1284698 buff +void sporelords_mycelium( special_effect_t& effect ) +{ + std::unordered_map buffs; + auto value = effect.driver()->effectN( 1 ).average( effect ); + + create_all_stat_buffs( effect, effect.trigger(), value, [ &buffs, value ]( stat_e s, buff_t* b ) { + // don't need a separate leech buff + if ( s == STAT_LEECH_RATING ) + return; + + // add leech stat + debug_cast( b )->add_stat_from_effect( 5, value ); + + buffs[ s ] = b; + } ); + + new dbc_proc_callback_t( effect.player, effect ); + + effect.player->callbacks.register_callback_execute_function( + effect.spell_id, [ buffs, p = effect.player ]( auto, auto, auto, auto ) { + auto stat = p->rng().range( secondary_ratings ); + for ( auto [ s, b ] : buffs ) + { + if ( s == stat ) + b->trigger(); + else + b->expire(); + } + } ); +} } // namespace trinkets namespace weapons @@ -3881,6 +3914,9 @@ void register_special_effects() register_special_effect( 1253112, trinkets::sylvan_wakrapuku ); register_special_effect( 1260633, trinkets::gloomspattered_dreadscale ); register_special_effect( 1260627, DISABLED_EFFECT ); // Gloom-Spattered Dreadscale Passive Driver + set_min_version( wowv_t( 12, 0, 7 ) ); + register_special_effect( 1284696, trinkets::sporelords_mycelium ); + reset_version_check(); // Weapons register_special_effect( { 1253357, 1253359 }, weapons::torments_duality ); // umbral sabre & radiant foil register_special_effect( 1266257, weapons::lightless_lament );