From 0be04233b00c3f1a284f0f51fafea90edbe8dd6c Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:24:45 -0600 Subject: [PATCH 01/17] [druid] Clean up trailing whitespace so my editor doesn't try to do it on every file save. --- engine/class_modules/sc_druid.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 26eb7b66a72..85be1084c8b 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -2925,7 +2925,7 @@ struct bear_attack_t : public druid_attack_t template struct druid_residual_action_t : public Base { -protected: +protected: using base_t = druid_residual_action_t; double residual_mul = 1.0; @@ -3337,7 +3337,7 @@ struct eclipse_buff_base_t : public druid_buff_t p()->buff.elunes_challenge->trigger(); if ( d == 0_ms ) - p()->buff.dreamstate->trigger(); + p()->buff.dreamstate->trigger(); } }; @@ -11911,7 +11911,7 @@ void druid_t::create_actions() } if ( talent.wild_guardian_3.ok() ) - active.vicious_brambles = get_secondary_action( "vicious_brambles", this ); + active.vicious_brambles = get_secondary_action( "vicious_brambles", this ); // Restoration if ( talent.yseras_gift.ok() ) @@ -12905,7 +12905,7 @@ void druid_t::init_special_effects() driver->spell_id = talent.twin_claw->id(); driver->proc_flags_ = PF_MELEE_ABILITY; // only melee abilities trigger driver->set_can_only_proc_from_class_abilities( true ); - + if ( specialization() == DRUID_FERAL ) { auto claw = get_secondary_action( "twin_claw", this, find_spell( 1271636 ) ); From ffb48a62fcee8a6bc69dcb27151a747b3a5ef946 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:25:25 -0600 Subject: [PATCH 02/17] [druid] Remove manual Matted Fur coefficient, as the value is now properly driven straight from effect value. --- engine/class_modules/sc_druid.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 85be1084c8b..5a483005876 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -3495,7 +3495,7 @@ struct matted_fur_buff_t final : public druid_absorb_buff_t matted_fur_buff_t( druid_t* p ) : base_t( p, "matted_fur", find_trigger( p->talent.matted_fur ).trigger() ), - coeff( find_trigger( p->talent.matted_fur ).percent() * 1.25 ) + coeff( find_trigger( p->talent.matted_fur ).percent() ) {} bool trigger( int s, double, double c, timespan_t d ) override From 21f9a895faa7934bab1e4893f96fd54f92d8eb43 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sat, 8 Aug 2026 22:50:27 -0600 Subject: [PATCH 03/17] [druid] Remove Guardian Apex. --- engine/class_modules/sc_druid.cpp | 213 ++---------------------------- 1 file changed, 11 insertions(+), 202 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 5a483005876..01185aee554 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -614,9 +614,6 @@ struct druid_t final : public parse_player_effects_t action_t* after_the_wildfire_heal; action_t* blazing_thorns; action_t* brambles_reflect; - action_t* echo_of_maul; - action_t* echo_of_ravage; - action_t* echo_of_raze; action_t* elunes_favored_heal; action_t* galactic_guardian; action_t* lunar_wrath; @@ -763,7 +760,6 @@ struct druid_t final : public parse_player_effects_t buff_t* celestial_might; // mid1 4pc buff_t* dream_guide; buff_t* dream_of_cenarius; - buff_t* echo_of_ironfur; buff_t* elunes_favored; buff_t* galactic_guardian; buff_t* gift_of_an_ancient_guardian; @@ -828,7 +824,6 @@ struct druid_t final : public parse_player_effects_t struct hots_t { dot_t* frenzied_regeneration; - dot_t* echo_of_frenzied_regeneration; } hots; // Cooldowns @@ -2444,47 +2439,6 @@ struct trigger_thriving_growth_t : public BASE } }; -template -struct trigger_wild_guardian_echo_base_t : public BASE -{ -protected: - using base_t = trigger_wild_guardian_echo_base_t; - action_t* echo_action = nullptr; - buff_t* echo_buff = nullptr; - timespan_t repeat_delay = 0_ms; - int num_repeat; - -public: - trigger_wild_guardian_echo_base_t( std::string_view n, druid_t* p, const spell_data_t* s, flag_e f ) - : BASE( n, p, s, f ), num_repeat( as( p->talent.wild_guardian_3->effectN( 3 ).base_value() ) ) - { - if ( num_repeat ) - repeat_delay = p->talent.wild_guardian_3->effectN( 4 ).time_value() / num_repeat; - } - - - virtual void trigger_echoes() - { - make_event( *BASE::sim, BASE::rng().range( WILD_GUARDIAN_ECHO_DELAY ), [ this ] { - echo_action->execute(); - - if ( repeat_delay > 0_ms ) - make_repeating_event( *BASE::sim, repeat_delay, [ this ] { echo_action->execute(); }, num_repeat ); - } ); - } - - void execute() override - { - BASE::execute(); - - if ( echo_action && echo_buff->check() ) - { - echo_buff->decrement(); - trigger_echoes(); - } - } -}; - template struct trigger_wildpower_surge_t : public BASE { @@ -3104,7 +3058,6 @@ struct bear_form_buff_t final : public druid_buff_t, public swap_melee_t else { p()->buff.ironfur->expire(); - p()->buff.echo_of_ironfur->expire(); } } ); } @@ -5290,25 +5243,8 @@ struct incapacitating_roar_t final : public bear_attack_t }; // Ironfur ================================================================== -struct ironfur_t final : public trigger_wild_guardian_echo_base_t> +struct ironfur_t final : public rage_spender_t { - struct echo_of_ironfur_t final : public druid_spell_t - { - echo_of_ironfur_t( druid_t* p ) - : druid_spell_t( "echo_of_ironfur", p, p->find_spell( 1269633 ), flag_e::WILDGUARDIAN ) - { - proc = background = true; - } - - void execute() override - { - druid_spell_t::execute(); - - // TODO: determine if duration increased by guardian of elune - p()->buff.echo_of_ironfur->trigger(); - } - }; - timespan_t goe_ext; DRUID_ABILITY( ironfur_t, base_t, "ironfur", p->talent.ironfur ), @@ -5316,12 +5252,6 @@ struct ironfur_t final : public trigger_wild_guardian_echo_base_ttalent.wild_guardian_1.ok() ) - { - echo_action = p->get_secondary_action( "echo_of_ironfur" ); - echo_buff = p->buff.gift_of_ironfur; - } } void execute() override @@ -5506,31 +5436,25 @@ struct mangle_t final : public use_fluid_form_tmaul_ravage_base_t:trigger_celestial_might_repeat_t:trigger_wild_guardian_echo_maul_t +|->maul_ravage_base_t:trigger_celestial_might_repeat_t | | | |->maul_t "maul" | -|->echo_of_maul_t "echo_of_maul" -| |->celestial_might_maul_t "maul_repeat" | |->raze_base_t | | -| |->maul_ravage_base_t:trigger_celestial_might_repeat_t:trigger_wild_guardian_echo_maul_t +| |->maul_ravage_base_t:trigger_celestial_might_repeat_t | | | | | |->raze_t "raze" | | | |->celestial_might_raze_t "raze_repeat" -| | -| |->echo_of_maul_t "echo_of_raze" | |->ravage_base_t | - |->ravage_maul_t:trigger_celestial_might_repeat_t:trigger_wild_guardian_echo_maul_t "ravage_maul" + |->ravage_maul_t:trigger_celestial_might_repeat_t "ravage_maul" | |->celestial_might_maul_t "ravage_repeat" - | - |->echo_of_maul_t "echo_of_ravage" */ struct maul_data_t @@ -5544,51 +5468,10 @@ struct maul_data_t }; template -struct trigger_wild_guardian_echo_maul_t : public trigger_wild_guardian_echo_base_t +struct trigger_celestial_might_repeat_t : public BASE { private: - using ab = trigger_wild_guardian_echo_base_t; - -public: - trigger_wild_guardian_echo_maul_t( std::string_view n, druid_t* p, const spell_data_t* s, flag_e f ) - : ab( n, p, s, f ) - { - if ( p->talent.wild_guardian_1.ok() ) - ab::echo_buff = p->buff.gift_of_maul; - } - - void trigger_echoes() override - { - // this maul and echo maul are all derived from maul_base_t so we can use ab::cast_state - make_event( *BASE::sim, BASE::rng().range( WILD_GUARDIAN_ECHO_DELAY ), - [ this, rage_mod = BASE::rage_modifier(), t = ab::target ] { - snapshot_and_execute( ab::echo_action, nullptr, false, [ this, rage_mod, t ]( auto, auto to ) { - auto state = ab::cast_state( to ); - state->rage_mod = rage_mod; - state->target = t; - ab::echo_action->set_target( t ); - } ); - - if ( ab::repeat_delay > 0_ms ) - { - make_repeating_event( *BASE::sim, ab::repeat_delay, [ this, rage_mod, t ] { - snapshot_and_execute( ab::echo_action, nullptr, false, [ this, rage_mod, t ]( auto, auto to ) { - auto state = ab::cast_state( to ); - state->rage_mod = rage_mod; - state->target = t; - ab::echo_action->set_target( t ); - } ); - }, ab::num_repeat ); - } - } ); - } -}; - -template -struct trigger_celestial_might_repeat_t : public trigger_wild_guardian_echo_maul_t -{ -private: - using ab = trigger_wild_guardian_echo_maul_t; + using ab = BASE; druid_t* p_; protected: @@ -5626,18 +5509,6 @@ struct trigger_celestial_might_repeat_t : public trigger_wild_guardian_echo_maul } }; -template -struct echo_of_maul_t : public BASE -{ - echo_of_maul_t( druid_t* p, std::string_view n, const spell_data_t* s ) : BASE( n, p, s, flag_e::WILDGUARDIAN ) - { - BASE::proc = BASE::background = true; - BASE::base_multiplier *= p->talent.wild_guardian_1->effectN( 1 ).percent(); - - BASE::kb_excess_rage_mul = 0.0; // echoes snapshot the kb rage multiplier and don't need to re-calculate - } -}; - template struct celestial_might_maul_t : public BASE { @@ -5812,9 +5683,6 @@ struct maul_ravage_base_t : public trigger_celestial_might_repeat_t // hasted gcd despite being 1s gcd_type = gcd_haste_type::ATTACK_HASTE; - if ( p->talent.wild_guardian_1.ok() ) - echo_action = p->active.echo_of_ravage; - if ( p->sets->has_set_bonus( DRUID_GUARDIAN, MID1, B4 ) ) { repeat_action = p->get_secondary_action>( name_str + "_repeat", &data() ); @@ -5864,9 +5732,6 @@ struct maul_t final : public maul_ravage_base_t if ( !data().ok() ) return; - if ( p->talent.wild_guardian_1.ok() ) - echo_action = p->active.echo_of_maul; - if ( p->sets->has_set_bonus( DRUID_GUARDIAN, MID1, B4 ) ) { repeat_action = p->get_secondary_action>( name_str + "_repeat", &data() ); @@ -5895,9 +5760,6 @@ struct raze_t final : public maul_ravage_base_t if ( !data().ok() ) return; - if ( p->talent.wild_guardian_1.ok() ) - echo_action = p->active.echo_of_raze; - if ( p->sets->has_set_bonus( DRUID_GUARDIAN, MID1, B4 ) ) { repeat_action = p->get_secondary_action>( name_str + "_repeat", &data() ); @@ -6700,35 +6562,10 @@ struct yseras_gift_t final : public druid_heal_t // Frenzied Regeneration ==================================================== // NOTE: this must come after regrowth and rejuvenation due to reinvigoration -struct frenzied_regeneration_t final : public trigger_wild_guardian_echo_base_t< - bear_attacks::rage_spender_t< +struct frenzied_regeneration_t final : public bear_attacks::rage_spender_t< trigger_call_of_the_elder_druid_t< - druid_heal_t>>> + druid_heal_t>> { - struct echo_of_frenzied_regeneration_t final : public druid_heal_t - { - echo_of_frenzied_regeneration_t( druid_t* p ) - : druid_heal_t( "echo_of_frenzied_regeneration", p, p->find_spell( 1269645 ), flag_e::WILDGUARDIAN ) - { - proc = background = true; - - // normalized based on duration, results in 17.5% less total healing - base_td_multiplier *= 1.0 + ( p->talent.frenzied_regeneration->duration() / data().duration() ); - - // scripted adjustment for increased tick time from reinvigoration, results 20% more total healing - base_td_multiplier *= - 1.0 + ( find_effect( p->talent.reinvigoration, this, A_ADD_PCT_MODIFIER, P_TICK_TIME ).percent() ); - - base_td_multiplier *= p->talent.wild_guardian_1->effectN( 1 ).percent(); - } - - dot_t* get_dot( [[maybe_unused]] player_t* t ) override - { - assert( t == player ); - return p()->hots.echo_of_frenzied_regeneration; - } - }; - action_t* regrowth = nullptr; action_t* rejuvenation = nullptr; double goe_mul; @@ -6757,12 +6594,6 @@ struct frenzied_regeneration_t final : public trigger_wild_guardian_echo_base_t< rejuvenation = get_reinvigoration_action( "rejuvenation" ); } - if ( p->talent.wild_guardian_1.ok() ) - { - echo_action = p->get_secondary_action( "echo_of_frenzied_regeneration" ); - echo_buff = p->buff.gift_of_frenzied_regeneration; - } - // verdant heart from FR doesn't apply to FR itself if ( p->talent.verdant_heart.ok() ) base_multiplier /= 1.0 + find_effect( &data(), A_MOD_HEALING_RECEIVED_PCT ).percent(); @@ -10707,9 +10538,6 @@ void druid_t::init_spells() if ( talent.frenzied_regeneration.ok() ) { hots.frenzied_regeneration = get_dot( "frenzied_regeneration", this ); - - if ( talent.wild_guardian_1.ok() ) - hots.echo_of_frenzied_regeneration = get_dot( "echo_of_frenzied_regeneration", this ); } // Arcane affinity is bugged with wrath and manually handled in wrath_t @@ -11365,14 +11193,6 @@ void druid_t::create_buffs() buff.dream_of_cenarius = make_fallback( talent.dream_of_cenarius_bear.ok(), this, "dream_of_cenarius", talent.dream_of_cenarius_bear->effectN( 1 ).trigger() ); - buff.echo_of_ironfur = - make_fallback( talent.wild_guardian_1.ok(), this, "echo_of_ironfur", find_spell( 1269633 ) ) - ->set_default_value( find_effect( talent.ironfur, A_MOD_ARMOR_BY_PRIMARY_STAT_PCT ).percent() * - talent.wild_guardian_1->effectN( 1 ).percent() ) - ->set_stack_behavior( buff_stack_behavior::ASYNCHRONOUS ) - ->add_invalidate( CACHE_AGILITY ) - ->add_invalidate( CACHE_ARMOR ); - buff.elunes_favored = make_fallback( talent.elunes_favored.ok(), this, "elunes_favored", spec.elunes_favored ) ->set_quiet( true ) ->set_freeze_stacks( true ) @@ -11902,14 +11722,6 @@ void druid_t::create_actions() active.waking_nightmare->add_child( pulse ); } - if ( talent.wild_guardian_1.ok() ) - { - active.echo_of_maul = get_secondary_action>( "echo_of_maul", find_spell( 1269648 ) ); - active.echo_of_raze = get_secondary_action>( "echo_of_raze", find_spell( 1269972 ) ); - active.echo_of_ravage = get_secondary_action>( "echo_of_ravage", find_spell( 1269973 ) ); - active.echo_of_ravage->name_str_reporting.clear(); - } - if ( talent.wild_guardian_3.ok() ) active.vicious_brambles = get_secondary_action( "vicious_brambles", this ); @@ -12020,9 +11832,6 @@ void druid_t::create_actions() }; find_parent( active.bursting_growth, "bloodseeker_vines" ); - find_parent( active.echo_of_maul, "wild_guardian" ); - find_parent( active.echo_of_ravage, "wild_guardian" ); - find_parent( active.echo_of_raze, "wild_guardian" ); find_parent( active.shooting_stars_mid1, "shooting_stars" ); find_parent( active.starfall_mid2, "starfall" ); find_parent( active.sundering_roar_thrash, "sundering_roar" ); @@ -13290,7 +13099,7 @@ void druid_t::invalidate_cache( cache_e c ) break; case CACHE_AGILITY: - if ( buff.ironfur->check() || buff.echo_of_ironfur->check() ) + if ( buff.ironfur->check() ) invalidate_cache( CACHE_ARMOR ); break; @@ -13309,9 +13118,9 @@ double druid_t::composite_armor() const { double a = player_t::composite_armor(); - if ( buff.ironfur->check() || buff.echo_of_ironfur->check() ) + if ( buff.ironfur->check() ) { - auto if_val = buff.ironfur->check_stack_value() + buff.echo_of_ironfur->check_stack_value(); + auto if_val = buff.ironfur->check_stack_value(); // TODO: confirm this is dynamic if ( buff.killing_strikes->check() ) From 56a0923db3b1c85f92935558ecf3d936b08c9cf9 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:04:30 -0600 Subject: [PATCH 04/17] [druid] Implement Gory Fur free Raze/Ravage/Maul procs. Add spell data to generator. --- dbc_extract3/dbc/generator.py | 1 + engine/class_modules/sc_druid.cpp | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/dbc_extract3/dbc/generator.py b/dbc_extract3/dbc/generator.py index df62955e44b..bca7a751eb1 100644 --- a/dbc_extract3/dbc/generator.py +++ b/dbc_extract3/dbc/generator.py @@ -2440,6 +2440,7 @@ class SpellDataGenerator(DataGenerator): # Guardian ( 1250913, 3 ), # Memory of Ysera heal ( 1269633, 3 ), ( 1269645, 3 ), ( 1269648, 3 ), # Apex talent echoes + ( 201671, 3 ), ( 1307881, 3 ), # Gory Fur # Restoration ), # Demon Hunter: diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 01185aee554..2ff81007a85 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -767,7 +767,8 @@ struct druid_t final : public parse_player_effects_t buff_t* gift_of_ironfur; buff_t* gift_of_maul; buff_t* gore; - buff_t* gory_fur; + buff_t* gory_fur_ironfur; + buff_t* gory_fur_maul_ravage; buff_t* guardian_of_elune; buff_t* incarnation_bear; buff_t* lunar_beam; @@ -5258,6 +5259,8 @@ struct ironfur_t final : public rage_spender_t { base_t::execute(); + p()->buff.gory_fur_maul_ravage->trigger( this ); + auto dur = p()->buff.ironfur->buff_duration(); if ( p()->buff.guardian_of_elune->check() ) @@ -5401,7 +5404,7 @@ struct mangle_t final : public use_fluid_form_tbuff.gory_fur->trigger( this ); + p()->buff.gory_fur_ironfur->trigger( this ); p()->buff.guardian_of_elune->trigger( this ); p()->buff.gore->consume( this ); @@ -11241,7 +11244,12 @@ void druid_t::create_buffs() buff.gore = make_fallback( talent.gore.ok(), this, "gore", find_spell( 93622 ) ) ->set_trigger_spell( talent.gore ); - buff.gory_fur = make_fallback( talent.gory_fur.ok(), this, "gory_fur", find_trigger( talent.gory_fur ).trigger() ) + buff.gory_fur_ironfur = make_fallback( talent.gory_fur.ok(), this, "gory_fur_ironfur", find_spell( 201671 ) ) + ->set_chance( talent.gory_fur->effectN( 2 ).percent() ) + ->set_trigger_spell( talent.gory_fur ); + + buff.gory_fur_maul_ravage = make_fallback( talent.gory_fur.ok(), this, "gory_fur_maul_ravage", find_spell( 1307881 ) ) + ->set_chance( talent.gory_fur->effectN( 1 ).percent() ) ->set_trigger_spell( talent.gory_fur ); buff.guardian_of_elune = make_fallback( talent.guardian_of_elune.ok(), @@ -14058,7 +14066,8 @@ void druid_t::parse_action_effects( action_t* action ) // snapshots regrowth hot, so disable the effect _a->parse_effects( buff.dream_of_cenarius, effect_mask_t( true ).disable( 5 ), CONSUME_BUFF ); - _a->parse_effects( buff.gory_fur, CONSUME_BUFF ); + _a->parse_effects( buff.gory_fur_ironfur, CONSUME_BUFF ); + _a->parse_effects( buff.gory_fur_maul_ravage, CONSUME_BUFF ); if ( talent.penumbral_swell.ok() ) _a->parse_effects( buff.lunar_beam ); From 61580e3286542cf739139d886d3b14e9fe6822fe Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 00:18:18 -0600 Subject: [PATCH 05/17] [druid] Guardian Midnight Season 2 2pc set bonus --- engine/class_modules/sc_druid.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 2ff81007a85..1a0d7121594 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -767,6 +767,7 @@ struct druid_t final : public parse_player_effects_t buff_t* gift_of_ironfur; buff_t* gift_of_maul; buff_t* gore; + buff_t* gorestained_claws; // mid2 2pc buff_t* gory_fur_ironfur; buff_t* gory_fur_maul_ravage; buff_t* guardian_of_elune; @@ -5864,6 +5865,8 @@ struct thrash_t final : public trigger_claw_rampage_tbuff.gorestained_claws->trigger( this ); + if ( rng().roll( fc_pct ) ) make_event( *sim, 500_ms, [ this ]() { p()->active.thrash_flashing->execute_on_target( target ); } ); @@ -11244,6 +11247,10 @@ void druid_t::create_buffs() buff.gore = make_fallback( talent.gore.ok(), this, "gore", find_spell( 93622 ) ) ->set_trigger_spell( talent.gore ); + buff.gorestained_claws = make_fallback( sets->has_set_bonus( DRUID_GUARDIAN, MID2, B2 ), this, "gorestained_claws", find_trigger( sets->set( DRUID_GUARDIAN, MID2, B2 ) ).trigger() ) + ->set_chance( sets->set( DRUID_GUARDIAN, MID2, B2 ).trigger()->effectN( 1 ).percent() ) + ->set_trigger_spell( sets->set( DRUID_GUARDIAN, MID2, B2 ) ); + buff.gory_fur_ironfur = make_fallback( talent.gory_fur.ok(), this, "gory_fur_ironfur", find_spell( 201671 ) ) ->set_chance( talent.gory_fur->effectN( 2 ).percent() ) ->set_trigger_spell( talent.gory_fur ); @@ -14066,6 +14073,7 @@ void druid_t::parse_action_effects( action_t* action ) // snapshots regrowth hot, so disable the effect _a->parse_effects( buff.dream_of_cenarius, effect_mask_t( true ).disable( 5 ), CONSUME_BUFF ); + _a->parse_effects( buff.gorestained_claws, CONSUME_BUFF ); _a->parse_effects( buff.gory_fur_ironfur, CONSUME_BUFF ); _a->parse_effects( buff.gory_fur_maul_ravage, CONSUME_BUFF ); From 58a524b9e0934abb96adcef0803f275f56da8313 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:22:14 -0600 Subject: [PATCH 06/17] [druid] Remove Guardian Apex even more. --- engine/class_modules/sc_druid.cpp | 96 +------------------------------ 1 file changed, 2 insertions(+), 94 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 1a0d7121594..2143939f7fc 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -45,8 +45,6 @@ static constexpr size_t UNSEEN_SWIPE_TARGETS = 3; static constexpr double UNSEEN_SWIPE_REDUCED_AOE = 5; // ursoc's fury & natural resilience cap as percentage of max hp static constexpr double GUARDIAN_SHIELD_CAP_HP_PCT = 0.3; -// wild guardian echo delays -static constexpr std::array WILD_GUARDIAN_ECHO_DELAY = { 300_ms, 400_ms }; namespace pets { @@ -621,7 +619,6 @@ struct druid_t final : public parse_player_effects_t action_t* memory_of_ysera_heal; action_t* sundering_roar_thrash; action_t* thrash_flashing; - action_t* vicious_brambles; action_t* waking_nightmare; // placeholder action_t* waking_nightmare_pulse; @@ -779,7 +776,6 @@ struct druid_t final : public parse_player_effects_t buff_t* sundering_roar; buff_t* ursocs_fury; buff_t* waking_nightmare; // proxy buff to track stack uptime - buff_t* wild_guardian; // enable replacement action // Restoration buff_t* abundance; @@ -5161,8 +5157,6 @@ struct berserk_bear_base_t : public bear_attack_t if ( p()->cooldown.thrash ) p()->cooldown.thrash->reset( true ); - - p()->buff.wild_guardian->trigger(); } }; @@ -5489,19 +5483,11 @@ struct trigger_celestial_might_repeat_t : public BASE void execute() override { - auto gift_stacks = p_->buff.gift_of_maul->check(); - ab::execute(); if ( repeat_action && p_->buff.celestial_might->consume( this ) ) { - auto delay = ab::rng().range( WILD_GUARDIAN_ECHO_DELAY ); - - // if gift was consumed for an echo, the repeat is delayed until after the echo - if ( gift_stacks - p_->buff.gift_of_maul->check() > 0 ) - delay += ab::rng().range( WILD_GUARDIAN_ECHO_DELAY ); - - make_event( *ab::sim, delay, [ this, rage_mod = BASE::rage_modifier(), t = ab::target ] { + make_event( *ab::sim, 0_ms, [ this, rage_mod = BASE::rage_modifier(), t = ab::target ] { snapshot_and_execute( repeat_action, nullptr, false, [ this, rage_mod, t ]( auto, auto to ) { auto state = ab::cast_state( to ); state->rage_mod = rage_mod; @@ -5537,14 +5523,12 @@ struct maul_base_t : public trigger_aggravate_wounds_ttalent.harnessed_rage->effectN( 1 ).base_value() ), - hr_gore_chance_pct( p->talent.harnessed_rage->effectN( 2 ).percent() ), - vb_mul( p->talent.wild_guardian_3->effectN( 1 ).percent() ) + hr_gore_chance_pct( p->talent.harnessed_rage->effectN( 2 ).percent() ) { add_option( opt_bool( "max_rage", max_rage ) ); @@ -5602,14 +5586,6 @@ struct maul_base_t : public trigger_aggravate_wounds_tresult_amount ) - residual_action::trigger( p()->active.vicious_brambles, s->target, s->result_amount * vb_mul ); - } - void consume_resource() override { base_t::consume_resource(); @@ -5892,15 +5868,6 @@ struct thrash_t final : public trigger_claw_rampage_t -{ - vicious_brambles_t( druid_t* p ) : residual_action_t( "vicious_brambles", p, p->find_spell( 1270065 ) ) - { - proc = true; - } -}; } // end namespace bear_attacks namespace heals @@ -8685,41 +8652,6 @@ struct wild_charge_t final : public druid_spell_t } }; -// Wild Guardian ============================================================ -struct wild_guardian_t final : public druid_spell_t -{ - int dream_charges; - - DRUID_ABILITY( wild_guardian_t, druid_spell_t, "wild_guardian", - p->talent.wild_guardian_1.ok() ? p->find_spell( 1269658 ) : spell_data_t::not_found() ), - dream_charges( as( p->talent.wild_guardian_2->effectN( 2 ).base_value() ) ) - {} - - bool ready() override - { - if ( !p()->buff.wild_guardian->check() ) - return false; - - return druid_spell_t::ready(); - } - - void execute() override - { - druid_spell_t::execute(); - - p()->buff.wild_guardian->expire(); - p()->buff.gift_of_frenzied_regeneration->trigger(); - p()->buff.gift_of_ironfur->trigger(); - p()->buff.gift_of_maul->trigger(); - - if ( dream_charges ) - { - p()->buff.dream_guide->trigger( dream_charges ); - p()->buff.dream_of_cenarius->trigger( dream_charges ); - } - } -}; - // Wild Mushroom ============================================================ struct wild_mushroom_t final : public druid_spell_t { @@ -10021,7 +9953,6 @@ action_t* druid_t::create_action( std::string_view name, std::string_view opt ) name == "red_moon" ) a = new red_moon_t( this ); else if ( name == "moonfire_only" ) a = new moonfire_t( this ); else if ( name == "sundering_roar" ) a = new sundering_roar_t( this ); - else if ( name == "wild_guardian" ) a = new wild_guardian_t( this ); // Restoration else if ( name == "efflorescence" ) a = new efflorescence_t( this ); @@ -11226,24 +11157,6 @@ void druid_t::create_buffs() } ); } - buff.gift_of_frenzied_regeneration = - make_fallback( talent.wild_guardian_1.ok() && ( talent.berserk_bear.ok() || talent.incarnation_bear.ok() ), - this,"gift_of_frenzied_regeneration", find_spell( 1269661 ) ) - ->set_initial_stack_to_max_stack() - ->set_consume_all_stacks( false ); - - buff.gift_of_ironfur = - make_fallback( talent.wild_guardian_1.ok() && ( talent.berserk_bear.ok() || talent.incarnation_bear.ok() ), - this, "gift_of_ironfur", find_spell( 1269659 ) ) - ->set_initial_stack_to_max_stack() - ->set_consume_all_stacks( false ); - - buff.gift_of_maul = - make_fallback( talent.wild_guardian_1.ok() && ( talent.berserk_bear.ok() || talent.incarnation_bear.ok() ), - this, "gift_of_maul", find_spell( 1269660 ) ) - ->set_initial_stack_to_max_stack() - ->set_consume_all_stacks( false ); - buff.gore = make_fallback( talent.gore.ok(), this, "gore", find_spell( 93622 ) ) ->set_trigger_spell( talent.gore ); @@ -11303,8 +11216,6 @@ void druid_t::create_buffs() this, "waking_nightmare", find_trigger( talent.waking_nightmare ).trigger() ) ->set_proc_callbacks( false ); - buff.wild_guardian = make_fallback( talent.wild_guardian_1.ok(), this, "wild_guardian", find_spell( 1269616 ) ); - // Restoration buffs buff.abundance = make_fallback( talent.abundance.ok(), this, "abundance", find_spell( 207640 ) ) ->set_duration( 0_ms ); @@ -11737,9 +11648,6 @@ void druid_t::create_actions() active.waking_nightmare->add_child( pulse ); } - if ( talent.wild_guardian_3.ok() ) - active.vicious_brambles = get_secondary_action( "vicious_brambles", this ); - // Restoration if ( talent.yseras_gift.ok() ) active.yseras_gift = get_secondary_action( "yseras_gift" ); From ed3f52494c0aa188cd2f181fe1edcdc879a6137f Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:22:31 -0600 Subject: [PATCH 07/17] [druid] Guardian Midnight Season 2 4pc set bonus --- engine/class_modules/sc_druid.cpp | 75 ++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 2143939f7fc..b4ad86c7e94 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -617,6 +617,7 @@ struct druid_t final : public parse_player_effects_t action_t* lunar_wrath; action_t* lunar_wrath_heal; action_t* memory_of_ysera_heal; + action_t* rampant_thorn; // mid2 4pc action_t* sundering_roar_thrash; action_t* thrash_flashing; action_t* waking_nightmare; // placeholder @@ -5733,6 +5734,67 @@ struct maul_t final : public maul_ravage_base_t } }; +struct rampant_thorn_t final : public bear_attack_t +{ + enum damage_e + { + SINGLETARGET, + AOE + }; + + template + struct damage_t final : public bear_attack_t + { + damage_t( druid_t* p, const spell_data_t* sd ) : + bear_attack_t( TYPE == SINGLETARGET ? "rampant_thorn_singletarget" : TYPE == AOE ? "rampant_thorn_aoe" : "default", p, sd ) + { + size_t index = 0; + switch ( TYPE ) + { + case SINGLETARGET: + aoe = 1; + index = 1; + break; + case AOE: + // tooltip interpolates $i which is max_targets(), but the value is not set + aoe = p->sets->set( DRUID_GUARDIAN, MID2, B4 )->effectN( 4 ).base_value(); + index = 2; + break; + default: + assert( false ); + } + + spell_power_mod.direct = data().effectN( index ).ap_coeff(); + } + }; + + action_t* singletarget; + action_t* aoe; + + rampant_thorn_t( druid_t* p ) + : bear_attack_t( "rampant_thorn", p, spell_data_t::nil() ), + singletarget( nullptr ), + aoe( nullptr ) + { + dual = background = proc = true; + + const spell_data_t* spell = p->find_spell( 1301157 ); + singletarget = new damage_t( p, spell ); + aoe = new damage_t( p, spell ); + + add_child( singletarget ); + add_child( aoe ); + } + + void execute() override + { + base_t::execute(); + + singletarget->execute(); + aoe->execute(); + } +}; + struct raze_t final : public maul_ravage_base_t { DRUID_ABILITY( raze_t, base_t, "raze", p->talent.raze ) @@ -5842,6 +5904,10 @@ struct thrash_t final : public trigger_claw_rampage_tbuff.gorestained_claws->trigger( this ); + if ( p()->sets->has_set_bonus( DRUID_GUARDIAN, MID2, B4 ) ) + { + p()->active.rampant_thorn->execute(); + } if ( rng().roll( fc_pct ) ) make_event( *sim, 500_ms, [ this ]() { p()->active.thrash_flashing->execute_on_target( target ); } ); @@ -11161,7 +11227,7 @@ void druid_t::create_buffs() ->set_trigger_spell( talent.gore ); buff.gorestained_claws = make_fallback( sets->has_set_bonus( DRUID_GUARDIAN, MID2, B2 ), this, "gorestained_claws", find_trigger( sets->set( DRUID_GUARDIAN, MID2, B2 ) ).trigger() ) - ->set_chance( sets->set( DRUID_GUARDIAN, MID2, B2 ).trigger()->effectN( 1 ).percent() ) + ->set_chance( sets->set( DRUID_GUARDIAN, MID2, B2 )->effectN( 1 ).percent() ) ->set_trigger_spell( sets->set( DRUID_GUARDIAN, MID2, B2 ) ); buff.gory_fur_ironfur = make_fallback( talent.gory_fur.ok(), this, "gory_fur_ironfur", find_spell( 201671 ) ) @@ -11637,6 +11703,11 @@ void druid_t::create_actions() active.sundering_roar_thrash = sr_thrash; } + if ( sets->has_set_bonus( DRUID_GUARDIAN, MID2, B4 ) ) + { + active.rampant_thorn = get_secondary_action( "rampant_thorn", this ); + } + if ( talent.waking_nightmare.ok() ) { active.waking_nightmare = new waking_nightmare_t( this ); @@ -11760,7 +11831,7 @@ void druid_t::create_actions() find_parent( active.sundering_roar_thrash, "sundering_roar" ); find_parent( active.the_light_of_elune, "moonfire" ); find_parent( active.thrash_flashing, "thrash" ); - find_parent( active.vicious_brambles, "wild_guardian" ); + find_parent( active.rampant_thorn, "thrash" ); // shroom madness find_parent( active.fungal_growth, "wild_mushroom" ); From 97e4bc85839c32208270592dd3a4f004b4a224b1 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:23:51 -0600 Subject: [PATCH 08/17] [druid] Offspec Starfire aoe damage bug is fixed. --- engine/class_modules/sc_druid.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index b4ad86c7e94..59bee9bc543 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -8338,11 +8338,7 @@ struct starfire_t : public use_fluid_form_t aoe = -1; reduced_aoe_targets = data().effectN( p->specialization() == DRUID_BALANCE ? 5 : 3 ).base_value(); - // offspec starfire seems to do same splash as balance starfire - if ( p->bugs ) - base_aoe_multiplier *= p->find_spell( 194153 )->effectN( 3 ).percent(); - else - base_aoe_multiplier *= data().effectN( p->specialization() == DRUID_BALANCE ? 3 : 2 ).percent(); + base_aoe_multiplier *= data().effectN( p->specialization() == DRUID_BALANCE ? 3 : 2 ).percent(); base_aoe_multiplier /= 1.0 + find_effect( p->talent.lunar_calling, &data() ).percent(); From b61d33be7f8adb09734118becfa04342fd277b38 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 01:57:31 -0600 Subject: [PATCH 09/17] [druid] Trigger tier set 3 times with a 250ms delay between each trigger. Prevent aoe component from hitting primary target. --- engine/class_modules/sc_druid.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 59bee9bc543..95d0d497ded 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -5766,6 +5766,16 @@ struct rampant_thorn_t final : public bear_attack_t spell_power_mod.direct = data().effectN( index ).ap_coeff(); } + + std::vector &target_list() const override + { + auto &tl = bear_attack_t::target_list(); + + if ( TYPE == AOE ) + range::erase_remove( tl, [ this ]( const auto &t ) { return t == target; } ); + + return tl; + } }; action_t* singletarget; @@ -5790,8 +5800,8 @@ struct rampant_thorn_t final : public bear_attack_t { base_t::execute(); - singletarget->execute(); - aoe->execute(); + singletarget->execute_on_target( target ); + aoe->execute_on_target( target ); } }; @@ -5906,7 +5916,8 @@ struct thrash_t final : public trigger_claw_rampage_tbuff.gorestained_claws->trigger( this ); if ( p()->sets->has_set_bonus( DRUID_GUARDIAN, MID2, B4 ) ) { - p()->active.rampant_thorn->execute(); + for ( size_t i = 1; i < 4; i++ ) + make_event( *sim, i * 250_ms, [ this ]() { p()->active.rampant_thorn->execute_on_target( target ); } ); } if ( rng().roll( fc_pct ) ) From c1b0e8804f77c712764e4a193462c257ada903eb Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Sun, 9 Aug 2026 21:18:57 -0600 Subject: [PATCH 10/17] [druid] Update Lunation to be a static cooldown modifier only for Guardian. --- engine/class_modules/sc_druid.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 95d0d497ded..6f55859f986 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -1869,8 +1869,8 @@ struct druid_action_t : public parse_action_effects_t, public druid_action { assert( p()->talent.lunation->effects().size() == 3 ); - static constexpr std::array lunation_cds = { - &druid_t::cooldowns_t::fury_of_elune, &druid_t::cooldowns_t::moon_cd, &druid_t::cooldowns_t::lunar_beam }; + static constexpr std::array lunation_cds = { + &druid_t::cooldowns_t::fury_of_elune, &druid_t::cooldowns_t::moon_cd }; for ( auto eff : p()->talent.lunation->effects() ) if ( auto cd = std::invoke( lunation_cds[ eff.index() ], p()->cooldown ) ) @@ -1880,7 +1880,8 @@ struct druid_action_t : public parse_action_effects_t, public druid_action bool can_trigger_lunation() const { - if ( !p()->talent.lunation.ok() || ab::background ) + // guardian druid lunation is a static cooldown duration modifier + if ( !p()->talent.lunation.ok() || ab::background || specialization() == DRUID_GUARDIAN ) return false; switch ( ab::id ) @@ -10550,6 +10551,10 @@ void druid_t::init_spells() hots.frenzied_regeneration = get_dot( "frenzied_regeneration", this ); } + // non-guardian lunation is still a proc effect + if ( specialization() != DRUID_GUARDIAN ) + deregister_passive_spell( talent.lunation->effectN( 3 ) ); + // Arcane affinity is bugged with wrath and manually handled in wrath_t register_passive_affect_list( talent.arcane_affinity, affect_list_t( 1 ).remove_family_flag( 0 ) ); From 0c135a8f3437cd62a155527dfbf3fb0dbba7c08f Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:13:48 -0600 Subject: [PATCH 11/17] [druid] Implement Wild Guardian 1. Correct errors. --- engine/class_modules/sc_druid.cpp | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 6f55859f986..4e0805e0608 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -618,6 +618,7 @@ struct druid_t final : public parse_player_effects_t action_t* lunar_wrath_heal; action_t* memory_of_ysera_heal; action_t* rampant_thorn; // mid2 4pc + action_t* spirits_wrath; // wild guardian 1 damage action_t* sundering_roar_thrash; action_t* thrash_flashing; action_t* waking_nightmare; // placeholder @@ -751,6 +752,7 @@ struct druid_t final : public parse_player_effects_t // Guardian buff_t* after_the_wildfire; + buff_t* answered_calling; // wild guardian 1 buff_t* berserk_bear; buff_t* blood_frenzy; buff_t* brambles; @@ -1856,6 +1858,12 @@ struct druid_action_t : public parse_action_effects_t, public druid_action ab::execute(); + if ( p()->talent.wild_guardian_1.ok() && ab::base_costs[ RESOURCE_RAGE ] > 0 ) + { + if ( p()->rng().roll( p()->talent.wild_guardian_1->effectN( 2 ).percent() ) ) + p()->buff.answered_calling->trigger( this ); + } + if ( !has_flag( flag_e::ALLOWSTEALTH ) ) { if ( p()->talent.strategic_infusion.ok() && p()->buff.prowl->check() ) @@ -1881,7 +1889,7 @@ struct druid_action_t : public parse_action_effects_t, public druid_action bool can_trigger_lunation() const { // guardian druid lunation is a static cooldown duration modifier - if ( !p()->talent.lunation.ok() || ab::background || specialization() == DRUID_GUARDIAN ) + if ( !p()->talent.lunation.ok() || ab::background || p()->specialization() == DRUID_GUARDIAN ) return false; switch ( ab::id ) @@ -5401,6 +5409,9 @@ struct mangle_t final : public use_fluid_form_tbuff.answered_calling->up() ) + p()->active.spirits_wrath->execute_on_target( target ); + p()->buff.gory_fur_ironfur->trigger( this ); p()->buff.guardian_of_elune->trigger( this ); @@ -5914,6 +5925,9 @@ struct thrash_t final : public trigger_claw_rampage_tbuff.answered_calling->up() ) + p()->active.spirits_wrath->execute_on_target( target ); + p()->buff.gorestained_claws->trigger( this ); if ( p()->sets->has_set_bonus( DRUID_GUARDIAN, MID2, B4 ) ) { @@ -10057,7 +10071,7 @@ action_t* druid_t::create_action( std::string_view name, std::string_view opt ) else if ( name == "soothe" ) a = new soothe_t( this ); else if ( name == "stampeding_roar" ) a = new stampeding_roar_t( this ); else if ( name == "starfire" ) a = new starfire_t( this ); - else if ( name == "starsurge" )a = new starsurge_offspec_t( this ); + else if ( name == "starsurge" ) a = new starsurge_offspec_t( this ); else if ( name == "sunfire" ) a = new sunfire_t( this ); else if ( name == "swipe" ) a = new swipe_proxy_t( this ); else if ( name == "swipe_bear" ) a = new swipe_bear_t( this ); @@ -10553,7 +10567,7 @@ void druid_t::init_spells() // non-guardian lunation is still a proc effect if ( specialization() != DRUID_GUARDIAN ) - deregister_passive_spell( talent.lunation->effectN( 3 ) ); + deregister_passive_spell( talent.lunation ); // Arcane affinity is bugged with wrath and manually handled in wrath_t register_passive_affect_list( talent.arcane_affinity, affect_list_t( 1 ).remove_family_flag( 0 ) ); @@ -11159,6 +11173,9 @@ void druid_t::create_buffs() ->set_default_value( talent.after_the_wildfire->effectN( 2 ).base_value() ) ->set_max_stack( 1 ); + buff.answered_calling = make_fallback( talent.wild_guardian_1.ok(), this, "answered_calling", find_trigger( talent.wild_guardian_1 ).trigger() ) + ->set_trigger_spell( talent.wild_guardian_1 ); + buff.berserk_bear = make_fallback( talent.berserk_bear.ok(), this, "berserk_bear", talent.berserk_bear ) ->set_name_reporting( "berserk" ) ->set_cooldown( 0_ms ) @@ -11731,6 +11748,13 @@ void druid_t::create_actions() active.waking_nightmare->add_child( pulse ); } + if ( talent.wild_guardian_1->ok() ) + { + auto s_data = find_spell( 1308093 ); + active.spirits_wrath = get_secondary_action( "spirits_wrath", this, s_data ); + active.spirits_wrath->proc = true; + } + // Restoration if ( talent.yseras_gift.ok() ) active.yseras_gift = get_secondary_action( "yseras_gift" ); From 81366f4571d5d2f21b0cb7837636064605a36b70 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:35:19 -0600 Subject: [PATCH 12/17] [druid] Reimplement Vicious Brambles as a template. --- engine/class_modules/sc_druid.cpp | 47 +++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 4e0805e0608..01226679633 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -621,6 +621,7 @@ struct druid_t final : public parse_player_effects_t action_t* spirits_wrath; // wild guardian 1 damage action_t* sundering_roar_thrash; action_t* thrash_flashing; + action_t* vicious_brambles; // wild guardian 2 damage action_t* waking_nightmare; // placeholder action_t* waking_nightmare_pulse; @@ -5350,12 +5351,43 @@ struct lunar_beam_t final : public druid_spell_t } }; +// Vicious Brambles========================================================== +template +struct trigger_vicious_brambles_t : public BASE +{ +private: + using ab = BASE; + double multiplier; + +protected: + using base_t = trigger_vicious_brambles_t; + +public: + trigger_vicious_brambles_t( std::string_view n, druid_t* p, const spell_data_t* s, flag_e f ) + : ab( n, p, s, f ), multiplier( 0.0 ) + { + if ( !p->talent.wild_guardian_2.ok() ) + return; + + multiplier = p->talent.wild_guardian_2->effectN( 2 ).percent(); + } + + void impact( action_state_t* s ) override + { + ab::impact( s ); + + if ( multiplier && s->result_amount ) + residual_action::trigger( ab::p()->active.vicious_brambles, s->target, s->result_amount * multiplier ); + } +}; + // Mangle =================================================================== struct mangle_t final : public use_fluid_form_t>>> + bear_attack_t>>>>> { action_t* strike = nullptr; gain_t* red_moon_gain = nullptr; @@ -5526,10 +5558,11 @@ struct celestial_might_maul_t : public BASE } }; -struct maul_base_t : public trigger_aggravate_wounds_t>>> + rage_spender_t>>>> { double hr_rage_threshold; double hr_gore_chance_pct; @@ -11755,6 +11788,14 @@ void druid_t::create_actions() active.spirits_wrath->proc = true; } + if ( talent.wild_guardian_2->ok() ) + { + using vicious_brambles_t = residual_action::residual_periodic_action_t; + auto s_data = find_spell( 1270065 ); + active.vicious_brambles = get_secondary_action( "vicious_brambles", this, s_data ); + active.vicious_brambles->proc = true; + } + // Restoration if ( talent.yseras_gift.ok() ) active.yseras_gift = get_secondary_action( "yseras_gift" ); From 85dc65c170c3f72a6ff82fb56b7fc74273576d53 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 02:38:55 -0600 Subject: [PATCH 13/17] [druid] Update Maul documentation. --- engine/class_modules/sc_druid.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 01226679633..5a2fd95d61c 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -5477,7 +5477,7 @@ struct mangle_t final : public use_fluid_form_tmaul_ravage_base_t:trigger_celestial_might_repeat_t | | From 6af4ffc1aa6064e3745c48fec877ed3c730148d4 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 03:23:24 -0600 Subject: [PATCH 14/17] [druid] Implement most of Wild Guardian 3. TODO: Wild Guardian force trigger action. --- engine/class_modules/sc_druid.cpp | 39 +++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 5a2fd95d61c..6bf232bfd22 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -754,6 +754,7 @@ struct druid_t final : public parse_player_effects_t // Guardian buff_t* after_the_wildfire; buff_t* answered_calling; // wild guardian 1 + buff_t* answered_calling_damage; // wild guardian 3 buff_t* berserk_bear; buff_t* blood_frenzy; buff_t* brambles; @@ -1862,7 +1863,10 @@ struct druid_action_t : public parse_action_effects_t, public druid_action if ( p()->talent.wild_guardian_1.ok() && ab::base_costs[ RESOURCE_RAGE ] > 0 ) { if ( p()->rng().roll( p()->talent.wild_guardian_1->effectN( 2 ).percent() ) ) + { p()->buff.answered_calling->trigger( this ); + p()->buff.answered_calling_damage->trigger( this ); + } } if ( !has_flag( flag_e::ALLOWSTEALTH ) ) @@ -5993,6 +5997,22 @@ struct thrash_t final : public trigger_claw_rampage_tfind_spell( 1308093 ) ) + { + proc = true; + + if ( !p->talent.wild_guardian_3.ok() ) + return; + + // rage generation if wild guardian 3 is talented + parse_effect_data( p->find_spell( 1308125 )->effectN( 1 ) ); + } +}; } // end namespace bear_attacks namespace heals @@ -11207,8 +11227,20 @@ void druid_t::create_buffs() ->set_max_stack( 1 ); buff.answered_calling = make_fallback( talent.wild_guardian_1.ok(), this, "answered_calling", find_trigger( talent.wild_guardian_1 ).trigger() ) + ->set_stack_change_callback( [ this ]( buff_t*, int, int _new ) { + if ( !_new || !talent.wild_guardian_3.ok() ) + return; + + if ( cooldown.mangle ) + cooldown.mangle->reset( true ); + if ( cooldown.thrash ) + cooldown.thrash->reset( true ); + } ) ->set_trigger_spell( talent.wild_guardian_1 ); + buff.answered_calling_damage = make_fallback( talent.wild_guardian_3.ok(), this, "answered_calling", find_spell( 1308647 ) ) + ->set_trigger_spell( talent.wild_guardian_3 ); + buff.berserk_bear = make_fallback( talent.berserk_bear.ok(), this, "berserk_bear", talent.berserk_bear ) ->set_name_reporting( "berserk" ) ->set_cooldown( 0_ms ) @@ -11782,11 +11814,7 @@ void druid_t::create_actions() } if ( talent.wild_guardian_1->ok() ) - { - auto s_data = find_spell( 1308093 ); - active.spirits_wrath = get_secondary_action( "spirits_wrath", this, s_data ); - active.spirits_wrath->proc = true; - } + active.spirits_wrath = get_secondary_action( "spirits_wrath", this ); if ( talent.wild_guardian_2->ok() ) { @@ -14115,6 +14143,7 @@ void druid_t::parse_action_effects( action_t* action ) _a->parse_effects( buff.unseen_predators_craving, talent.unseen_predator_2->effectN( 1 ).percent() ); // Guardian + _a->parse_effects( buff.answered_calling_damage ); _a->parse_effects( buff.berserk_bear, effect_mask_t( true ).disable( 6, 7, 8, 9 ) ); _a->parse_effects( buff.incarnation_bear, effect_mask_t( true ).disable( 6, 7, 8, 9, 14, 15, 16, 17 ) ); // additional effects if astral insight is talented From a343739e18c5d41c0eda126a9133c4c4d613d92d Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:21:23 -0600 Subject: [PATCH 15/17] [druid] Move Gory Fur Ironfur trigger to the correct parent action. --- engine/class_modules/sc_druid.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index 6bf232bfd22..ac931751009 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -5448,7 +5448,6 @@ struct mangle_t final : public use_fluid_form_tbuff.answered_calling->up() ) p()->active.spirits_wrath->execute_on_target( target ); - p()->buff.gory_fur_ironfur->trigger( this ); p()->buff.guardian_of_elune->trigger( this ); p()->buff.gore->consume( this ); @@ -5633,6 +5632,8 @@ struct maul_base_t : public trigger_vicious_brambles_t< if ( kb_excess_rage_mul ) kb_excess_rage = get_excess_rage(); + p()->buff.gory_fur_ironfur->trigger( this ); + base_t::execute(); } From f1517e76d402662136b71fc0e79c67fdca655df4 Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:01:09 -0600 Subject: [PATCH 16/17] [druid] Complete Wild Guardian 3 implementation. --- engine/class_modules/sc_druid.cpp | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/engine/class_modules/sc_druid.cpp b/engine/class_modules/sc_druid.cpp index ac931751009..19597e951c0 100644 --- a/engine/class_modules/sc_druid.cpp +++ b/engine/class_modules/sc_druid.cpp @@ -760,6 +760,7 @@ struct druid_t final : public parse_player_effects_t buff_t* brambles; buff_t* bristling_fur; buff_t* celestial_might; // mid1 4pc + buff_t* dream_conduit; // wild guardian 3 buff_t* dream_guide; buff_t* dream_of_cenarius; buff_t* elunes_favored; @@ -781,6 +782,7 @@ struct druid_t final : public parse_player_effects_t buff_t* sundering_roar; buff_t* ursocs_fury; buff_t* waking_nightmare; // proxy buff to track stack uptime + buff_t* wild_guardian; // wild guardian 3 // Restoration buff_t* abundance; @@ -5164,6 +5166,8 @@ struct berserk_bear_base_t : public bear_attack_t assert( buff ); buff->trigger(); + p()->buff.wild_guardian->trigger(); + if ( p()->cooldown.growl ) p()->cooldown.growl->reset( true ); @@ -8916,6 +8920,30 @@ struct wild_mushroom_t final : public druid_spell_t } }; +// Wild Guardian ============================================================ +struct wild_guardian_t final : public druid_spell_t +{ + DRUID_ABILITY( wild_guardian_t, druid_spell_t, "wild_guardian", + p->talent.wild_guardian_3.ok() ? p->find_spell( 1269658 ) : spell_data_t::not_found() ) + {} + + bool ready() override + { + if ( !p()->buff.wild_guardian->check() ) + return false; + + return druid_spell_t::ready(); + } + + void execute() override + { + druid_spell_t::execute(); + + p()->buff.wild_guardian->expire(); + p()->buff.dream_conduit->trigger(); + } +}; + // Wrath ==================================================================== struct wrath_t : public use_fluid_form_t { @@ -10095,6 +10123,7 @@ action_t* druid_t::create_action( std::string_view name, std::string_view opt ) name == "red_moon" ) a = new red_moon_t( this ); else if ( name == "moonfire_only" ) a = new moonfire_t( this ); else if ( name == "sundering_roar" ) a = new sundering_roar_t( this ); + else if ( name == "wild_guardian" ) a = new wild_guardian_t( this ); // Restoration else if ( name == "efflorescence" ) a = new efflorescence_t( this ); @@ -11284,6 +11313,9 @@ void druid_t::create_buffs() buff.celestial_might = make_fallback( sets->has_set_bonus( DRUID_GUARDIAN, MID1, B4 ), this, "celestial_might", find_trigger( sets->set( DRUID_GUARDIAN, MID1, B4 ) ).trigger() ); + buff.dream_conduit = make_fallback( talent.wild_guardian_3.ok(), this, "dream_conduit", find_trigger( find_spell( 1269658 ) ).trigger() ) + ->set_trigger_spell( talent.wild_guardian_3 ); + buff.dream_guide = make_fallback( talent.dream_guide.ok(), this, "dream_guide", find_trigger( talent.dream_guide ).trigger() ) ->set_consume_all_stacks( false ); @@ -11377,6 +11409,9 @@ void druid_t::create_buffs() this, "waking_nightmare", find_trigger( talent.waking_nightmare ).trigger() ) ->set_proc_callbacks( false ); + buff.wild_guardian = make_fallback( talent.wild_guardian_3.ok(), this, "wild_guardian", find_spell( 1269616 ) ) + ->set_trigger_spell( talent.wild_guardian_3 ); + // Restoration buffs buff.abundance = make_fallback( talent.abundance.ok(), this, "abundance", find_spell( 207640 ) ) ->set_duration( 0_ms ); From e58fe7bb726e3d8dc032f5f5cbf5a539e12a1ddc Mon Sep 17 00:00:00 2001 From: Kate Martin <51387586+renanthera@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:02:37 -0600 Subject: [PATCH 17/17] [dbc] Remove old Wild Guardian Echoes. Add Wild Guardian 3 usability tracker. --- dbc_extract3/dbc/generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dbc_extract3/dbc/generator.py b/dbc_extract3/dbc/generator.py index bca7a751eb1..56f836bb91f 100644 --- a/dbc_extract3/dbc/generator.py +++ b/dbc_extract3/dbc/generator.py @@ -2439,8 +2439,8 @@ class SpellDataGenerator(DataGenerator): ( 1280133, 2 ), # Rake (unused?) # Guardian ( 1250913, 3 ), # Memory of Ysera heal - ( 1269633, 3 ), ( 1269645, 3 ), ( 1269648, 3 ), # Apex talent echoes ( 201671, 3 ), ( 1307881, 3 ), # Gory Fur + ( 1269616, 3 ), # Wild Guardian Usable Buff # Restoration ), # Demon Hunter: