From e438ee41b7e95aab5ef2535e716cdab2ee7af203 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Mon, 19 Oct 2026 15:04:41 -0700 Subject: [PATCH 01/42] [player] Standardize spell rank table usage --- engine/class_modules/sc_shaman.cpp | 164 +++++++++++++---------------- engine/player/player.hpp | 46 ++++++++ 2 files changed, 117 insertions(+), 93 deletions(-) diff --git a/engine/class_modules/sc_shaman.cpp b/engine/class_modules/sc_shaman.cpp index 930e1b91ca..4ffac5e605 100644 --- a/engine/class_modules/sc_shaman.cpp +++ b/engine/class_modules/sc_shaman.cpp @@ -9,21 +9,6 @@ namespace { -struct spell_rank_t -{ - unsigned spell_id; -}; -struct flametongue_rank_t -{ - unsigned spell_id; // Flametongue Weapon - unsigned driver_id; // Flametongue Weapon Proc - unsigned attack_id; // Flametongue Attack -}; -struct rockbiter_rank_t -{ - unsigned spell_id; // Rockbiter Weapon - unsigned passive_id; // Rockbiter Weapon (Passive) -}; struct totem_rank_t { unsigned spell_id; // the totem @@ -31,73 +16,6 @@ struct totem_rank_t }; -template -auto max_rank( const player_t* player, const RANKS& ranks ) -{ - typename RANKS::value_type best {}; - unsigned best_level = 0; - - for ( const auto& rank : ranks ) - { - const spell_data_t* spell = player->find_spell( rank.spell_id ); - - if ( spell->ok() && spell->level() >= best_level ) - { - best = rank; - best_level = spell->level(); - } - } - - return best; -} - -constexpr std::array flametongue_weapon_ranks { - // spell driver attack - flametongue_rank_t{ 8024, 8026, 29469 }, - flametongue_rank_t{ 8027, 8028, 29470 }, - flametongue_rank_t{ 8030, 8029, 10444 }, - flametongue_rank_t{ 16339, 10445, 10444 }, - flametongue_rank_t{ 16341, 16343, 10444 }, - flametongue_rank_t{ 16342, 16344, 10444 }, -}; -constexpr std::array earth_shock_ranks { - // spell - spell_rank_t{ 8042 }, - spell_rank_t{ 8044 }, - spell_rank_t{ 8045 }, - spell_rank_t{ 8046 }, - spell_rank_t{ 10412 }, - spell_rank_t{ 10413 }, - spell_rank_t{ 10414 }, -}; -constexpr std::array flame_shock_ranks { - // spell - spell_rank_t{ 8050 }, - spell_rank_t{ 8052 }, - spell_rank_t{ 8053 }, - spell_rank_t{ 10447 }, - spell_rank_t{ 10448 }, - spell_rank_t{ 29228 }, -}; -constexpr std::array rockbiter_weapon_ranks { - // spell passive - rockbiter_rank_t{ 8017, 10400 }, - rockbiter_rank_t{ 8018, 15567 }, - rockbiter_rank_t{ 8019, 15568 }, - rockbiter_rank_t{ 10399, 15569 }, - rockbiter_rank_t{ 16314, 16311 }, - rockbiter_rank_t{ 16315, 16312 }, - rockbiter_rank_t{ 16316, 16313 }, -}; -constexpr std::array strength_of_earth_totem_ranks { - // spell aura - totem_rank_t{ 8075, 8076 }, - totem_rank_t{ 8160, 8162 }, - totem_rank_t{ 8161, 8163 }, - totem_rank_t{ 10442, 10441 }, - totem_rank_t{ 25361, 25362 }, -}; - struct shaman_t final : public player_t { shaman_t( sim_t* sim, util::string_view name, race_e r = RACE_NONE ) : @@ -250,10 +168,20 @@ void shaman_t::invalidate_cache( cache_e c ) // Earth Shock +constexpr std::array earth_shock_ranks { + // spell + spell_rank_t{ 8042 }, + spell_rank_t{ 8044 }, + spell_rank_t{ 8045 }, + spell_rank_t{ 8046 }, + spell_rank_t{ 10412 }, + spell_rank_t{ 10413 }, + spell_rank_t{ 10414 }, +}; struct earth_shock_t : public shaman_spell_t { earth_shock_t( shaman_t* player, util::string_view options_str ) : - shaman_spell_t( "earth_shock", player, player->find_spell( max_rank( player, earth_shock_ranks ).spell_id ) ) + shaman_spell_t( "earth_shock", player, player->find_spell( player->find_rank( earth_shock_ranks ).spell_id ) ) { parse_options( options_str ); @@ -264,10 +192,19 @@ struct earth_shock_t : public shaman_spell_t // Flame Shock +constexpr std::array flame_shock_ranks { + // spell + spell_rank_t{ 8050 }, + spell_rank_t{ 8052 }, + spell_rank_t{ 8053 }, + spell_rank_t{ 10447 }, + spell_rank_t{ 10448 }, + spell_rank_t{ 29228 }, +}; struct flame_shock_t : public shaman_spell_t { flame_shock_t( shaman_t* player, util::string_view options_str ) : - shaman_spell_t( "flame_shock", player, player->find_spell( max_rank( player, flame_shock_ranks ).spell_id ) ) + shaman_spell_t( "flame_shock", player, player->find_spell( player->find_rank( flame_shock_ranks ).spell_id ) ) { parse_options( options_str ); @@ -280,14 +217,30 @@ struct flame_shock_t : public shaman_spell_t // Shaman Weapon Imbues // ========================================================================== +struct flametongue_rank_t +{ + unsigned spell_id; // Flametongue Weapon + unsigned driver_id; // Flametongue Weapon Proc + unsigned attack_id; // Flametongue Attack +}; +constexpr std::array flametongue_weapon_ranks { + // spell driver attack + flametongue_rank_t{ 8024, 8026, 29469 }, + flametongue_rank_t{ 8027, 8028, 29470 }, + flametongue_rank_t{ 8030, 8029, 10444 }, + flametongue_rank_t{ 16339, 10445, 10444 }, + flametongue_rank_t{ 16341, 16343, 10444 }, + flametongue_rank_t{ 16342, 16344, 10444 }, +}; + struct flametongue_attack_t : public shaman_spell_t { flametongue_attack_t( shaman_t* player ) : - shaman_spell_t( "flametongue_attack", player, player->find_spell( max_rank( player, flametongue_weapon_ranks ).attack_id ) ) + shaman_spell_t( "flametongue_attack", player, player->find_spell( player->find_rank( flametongue_weapon_ranks ).attack_id ) ) { background = true; - auto rank = max_rank( player, flametongue_weapon_ranks ); + auto rank = player->find_rank( flametongue_weapon_ranks ); double proc_value = player->find_spell( rank.driver_id )->effectN( 1 ).average( player, player->level() ); double weapon_speed = player->equipped_weapons[ SLOT_MAIN_HAND ].swing_time.total_seconds(); @@ -299,7 +252,7 @@ struct flametongue_attack_t : public shaman_spell_t struct flametongue_weapon_t : public shaman_spell_t { flametongue_weapon_t( shaman_t* player, util::string_view options_str ) : - shaman_spell_t( "flametongue_weapon", player, player->find_spell( max_rank( player, flametongue_weapon_ranks ).spell_id ) ) + shaman_spell_t( "flametongue_weapon", player, player->find_spell( player->find_rank( flametongue_weapon_ranks ).spell_id ) ) { parse_options( options_str ); harmful = false; @@ -323,10 +276,27 @@ struct flametongue_weapon_t : public shaman_spell_t } }; +// rockbiter weapon + +struct rockbiter_rank_t +{ + unsigned spell_id; // Rockbiter Weapon + unsigned passive_id; // Rockbiter Weapon (Passive) +}; +constexpr std::array rockbiter_weapon_ranks { + // spell passive + rockbiter_rank_t{ 8017, 10400 }, + rockbiter_rank_t{ 8018, 15567 }, + rockbiter_rank_t{ 8019, 15568 }, + rockbiter_rank_t{ 10399, 15569 }, + rockbiter_rank_t{ 16314, 16311 }, + rockbiter_rank_t{ 16315, 16312 }, + rockbiter_rank_t{ 16316, 16313 }, +}; struct rockbiter_weapon_t : public shaman_spell_t { rockbiter_weapon_t( shaman_t* player, util::string_view options_str ) : - shaman_spell_t( "rockbiter_weapon", player, player->find_spell( max_rank( player, rockbiter_weapon_ranks ).spell_id ) ) + shaman_spell_t( "rockbiter_weapon", player, player->find_spell( player->find_rank( rockbiter_weapon_ranks ).spell_id ) ) { parse_options( options_str ); harmful = false; @@ -358,10 +328,18 @@ struct rockbiter_weapon_t : public shaman_spell_t // TODO: Make totems a unit for use with range? // TODO: Party buff +constexpr std::array strength_of_earth_totem_ranks { + // spell aura + totem_rank_t{ 8075, 8076 }, + totem_rank_t{ 8160, 8162 }, + totem_rank_t{ 8161, 8163 }, + totem_rank_t{ 10442, 10441 }, + totem_rank_t{ 25361, 25362 }, +}; struct strength_of_earth_totem_t : public shaman_spell_t { strength_of_earth_totem_t( shaman_t* player, util::string_view options_str ) : - shaman_spell_t( "strength_of_earth_totem", player, player->find_spell( max_rank( player, strength_of_earth_totem_ranks ).spell_id ) ) + shaman_spell_t( "strength_of_earth_totem", player, player->find_spell( player->find_rank( strength_of_earth_totem_ranks ).spell_id ) ) { parse_options( options_str ); harmful = false; @@ -468,13 +446,13 @@ void shaman_t::create_buffs() { player_t::create_buffs(); - buff.flametongue_weapon = make_buff( this, "flametongue_weapon", find_spell( max_rank( this, flametongue_weapon_ranks ).spell_id ) ); + buff.flametongue_weapon = make_buff( this, "flametongue_weapon", find_spell( find_rank( flametongue_weapon_ranks ).spell_id ) ); buff.rockbiter_weapon = make_buff( - this, "rockbiter_weapon", find_spell( max_rank( this, rockbiter_weapon_ranks ).passive_id ) ); + this, "rockbiter_weapon", find_spell( find_rank( rockbiter_weapon_ranks ).passive_id ) ); buff.strength_of_earth = make_buff( - this, "strength_of_earth", find_spell( max_rank( this, strength_of_earth_totem_ranks ).aura_id ) ) - ->set_duration( find_spell( max_rank( this, strength_of_earth_totem_ranks ).spell_id )->duration() ); + this, "strength_of_earth", find_spell( find_rank( strength_of_earth_totem_ranks ).aura_id ) ) + ->set_duration( find_spell( find_rank( strength_of_earth_totem_ranks ).spell_id )->duration() ); } void shaman_t::create_actions() diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 0600c13fe5..e1ae5fec94 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -98,6 +98,23 @@ struct player_report_extension_t virtual void html_customsection(report::sc_html_stream&) = 0; }; +/* Spell rank tables + * + * Spell rank tables group related spells together by rank to make it easier to connect auras/drivers/etc related + * to a primary spell. The basic version just has a single spell_id but it's easy to make custom versions with + * related spells. + */ +struct spell_rank_t +{ + unsigned spell_id; +}; + +enum spell_rank_e : int +{ + RANK_MAX_USABLE = 0, // highest rank usable at the player level + RANK_MAX = -1, // highest rank in the table +}; + struct player_t : public actor_t { static const int default_level = MAX_LEVEL; @@ -736,6 +753,35 @@ struct player_t : public actor_t const spell_data_t* find_spell( unsigned int id, specialization_e s ) const; const spell_data_t* find_spell( unsigned int id ) const; + template + auto find_rank( const RANKS& ranks, int rank = RANK_MAX_USABLE ) const + { + typename RANKS::value_type best {}; + + if ( rank == RANK_MAX ) + return ranks.back(); + + if ( rank > 0 ) + { + if ( static_cast( rank ) <= ranks.size() ) + best = ranks[ rank - 1 ]; + return best; + } + + unsigned best_level = 0; + for ( const auto& entry : ranks ) + { + auto spell = find_spell( entry.spell_id ); + if ( spell->ok() && spell->level() >= best_level ) + { + best = entry; + best_level = spell->level(); + } + } + + return best; + } + pet_t* find_pet( util::string_view name ) const; item_t* find_item_by_name( util::string_view name ); item_t* find_item_by_id( unsigned id ); From 24bebf1d26ccbbbf3251e8569b71bb077a969856 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:07:13 -0300 Subject: [PATCH 02/42] Smite base implementation --- engine/class_modules/sc_priest.cpp | 58 +++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 0df2395016..8e29a0d2af 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -5,6 +5,7 @@ // ========================================================================== #include "simulationcraft.hpp" +#include "action/parse_effects.hpp" namespace priestspace { @@ -12,6 +13,10 @@ struct priest_t final : public player_t { priest_t( sim_t* sim, util::string_view name, race_e r = RACE_NONE ) : player_t( sim, PRIEST, name, r ) {} + + resource_e primary_resource() const override { return RESOURCE_MANA; } + + action_t* create_action( util::string_view name, util::string_view options_str ) override; }; struct priest_module_t final : public module_t @@ -34,10 +39,61 @@ struct priest_module_t final : public module_t void register_actor_initializers( sim_t* ) const override {} }; +template +struct priest_action_t : public parse_action_effects_t +{ +private: + using ab = parse_action_effects_t; + +public: + using base_t = priest_action_t; + + template + priest_action_t( Args&&... args ) : ab( std::forward( args )... ) {} + + priest_t* p() + { return debug_cast( this->player ); } + + const priest_t* p() const + { return debug_cast( this->player ); } +}; + +using priest_spell_t = priest_action_t; + +// Smite + +constexpr std::array smite_ranks { + // spell + spell_rank_t{ 585 }, + spell_rank_t{ 591 }, + spell_rank_t{ 598 }, + spell_rank_t{ 984 }, + spell_rank_t{ 1004 }, + spell_rank_t{ 6060 }, + spell_rank_t{ 10933 }, + spell_rank_t{ 10934 }, +}; +struct smite_t : public priest_spell_t +{ + smite_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "smite", player, player->find_spell( player->find_rank( smite_ranks ).spell_id ) ) + { + parse_options( options_str ); + } +}; + +action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) +{ + if ( name == "smite" ) + return new smite_t( this, options_str ); + + return player_t::create_action( name, options_str ); +} + } // namespace priestspace const module_t* module_t::priest() { static priestspace::priest_module_t m; return &m; -} +} \ No newline at end of file From 42c176595a0386f25af3bf7e070b4e5d5b33f4cd Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:12:12 -0300 Subject: [PATCH 03/42] Base stats --- engine/class_modules/sc_priest.cpp | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 8e29a0d2af..da8b71a06a 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -16,6 +16,9 @@ struct priest_t final : public player_t resource_e primary_resource() const override { return RESOURCE_MANA; } + void init_base_stats() override; + void init_spells() override; + action_t* create_action( util::string_view name, util::string_view options_str ) override; }; @@ -60,6 +63,25 @@ struct priest_action_t : public parse_action_effects_t using priest_spell_t = priest_action_t; +void priest_t::init_base_stats() +{ + player_t::init_base_stats(); + + // original classic crit rates according to + // * https://github.com/wowsims/classic/blob/master/sim/core/base_stats.go#L106 + // TODO: Verify in-game + base.attack_crit_chance = get_passive_player_value( 0.03, "all_crit" ); + base.spell_crit_chance = get_passive_player_value( 0.008, "all_crit" ); +} + +void priest_t::init_spells() +{ + player_t::init_spells(); + + parse_all_class_passives(); + parse_all_passive_talents(); +} + // Smite constexpr std::array smite_ranks { From 33c964d613929af734cf09f100a07048f2e53d46 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:15:07 -0300 Subject: [PATCH 04/42] [Priest] Shadow Word: Pain --- engine/class_modules/sc_priest.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index da8b71a06a..e237c70168 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -104,10 +104,34 @@ struct smite_t : public priest_spell_t } }; +// Shadow Word: Pain + +constexpr std::array shadow_word_pain_ranks { + // spell + spell_rank_t{ 589 }, + spell_rank_t{ 594 }, + spell_rank_t{ 970 }, + spell_rank_t{ 992 }, + spell_rank_t{ 2767 }, + spell_rank_t{ 10892 }, + spell_rank_t{ 10893 }, + spell_rank_t{ 10894 }, +}; +struct shadow_word_pain_t : public priest_spell_t +{ + shadow_word_pain_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "shadow_word_pain", player, player->find_spell( player->find_rank( shadow_word_pain_ranks ).spell_id ) ) + { + parse_options( options_str ); + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) return new smite_t( this, options_str ); + if ( name == "shadow_word_pain" ) + return new shadow_word_pain_t( this, options_str ); return player_t::create_action( name, options_str ); } From 7a7d9cfaff04c0e8de44289e9d33367a7545f4fb Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:17:47 -0300 Subject: [PATCH 05/42] [Priest] Mind Blast --- engine/class_modules/sc_priest.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index e237c70168..2af78f6ba0 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -126,12 +126,37 @@ struct shadow_word_pain_t : public priest_spell_t } }; +// Mind Blast + +constexpr std::array mind_blast_ranks { + // spell + spell_rank_t{ 8092 }, + spell_rank_t{ 8102 }, + spell_rank_t{ 8103 }, + spell_rank_t{ 8104 }, + spell_rank_t{ 8105 }, + spell_rank_t{ 8106 }, + spell_rank_t{ 10945 }, + spell_rank_t{ 10946 }, + spell_rank_t{ 10947 }, +}; +struct mind_blast_t : public priest_spell_t +{ + mind_blast_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "mind_blast", player, player->find_spell( player->find_rank( mind_blast_ranks ).spell_id ) ) + { + parse_options( options_str ); + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) return new smite_t( this, options_str ); if ( name == "shadow_word_pain" ) return new shadow_word_pain_t( this, options_str ); + if ( name == "mind_blast" ) + return new mind_blast_t( this, options_str ); return player_t::create_action( name, options_str ); } From 061a11b7c1d16c1a078928a6494bf05cd9c9b395 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:20:56 -0300 Subject: [PATCH 06/42] [Priest] Devouring Plague --- engine/class_modules/sc_priest.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 2af78f6ba0..0f562807ec 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -149,6 +149,27 @@ struct mind_blast_t : public priest_spell_t } }; +// Devouring Plague + +// TODO: Heal the caster for the damage dealt +constexpr std::array devouring_plague_ranks { + // spell + spell_rank_t{ 2944 }, + spell_rank_t{ 19276 }, + spell_rank_t{ 19277 }, + spell_rank_t{ 19278 }, + spell_rank_t{ 19279 }, + spell_rank_t{ 19280 }, +}; +struct devouring_plague_t : public priest_spell_t +{ + devouring_plague_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "devouring_plague", player, player->find_spell( player->find_rank( devouring_plague_ranks ).spell_id ) ) + { + parse_options( options_str ); + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) @@ -157,6 +178,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new shadow_word_pain_t( this, options_str ); if ( name == "mind_blast" ) return new mind_blast_t( this, options_str ); + if ( name == "devouring_plague" ) + return new devouring_plague_t( this, options_str ); return player_t::create_action( name, options_str ); } From f4669bcd04075a81ea9a56b776ee069850e94109 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:25:23 -0300 Subject: [PATCH 07/42] [Priest] Mind Flay --- engine/class_modules/sc_priest.cpp | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 0f562807ec..e659f96554 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -20,6 +20,11 @@ struct priest_t final : public player_t void init_spells() override; action_t* create_action( util::string_view name, util::string_view options_str ) override; + + struct talents_t + { + player_talent_t mind_flay; + } talent; }; struct priest_module_t final : public module_t @@ -80,6 +85,8 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); + + talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); } // Smite @@ -170,6 +177,29 @@ struct devouring_plague_t : public priest_spell_t } }; +// Mind Flay + +constexpr std::array mind_flay_ranks { + // spell + spell_rank_t{ 15407 }, + spell_rank_t{ 17311 }, + spell_rank_t{ 17312 }, + spell_rank_t{ 17313 }, + spell_rank_t{ 17314 }, + spell_rank_t{ 18807 }, +}; +struct mind_flay_t : public priest_spell_t +{ + mind_flay_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "mind_flay", player, player->talent.mind_flay.ok() + ? player->find_spell( player->find_rank( mind_flay_ranks ).spell_id ) + : spell_data_t::not_found() ) + { + parse_options( options_str ); + channeled = true; + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) @@ -180,6 +210,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new mind_blast_t( this, options_str ); if ( name == "devouring_plague" ) return new devouring_plague_t( this, options_str ); + if ( name == "mind_flay" ) + return new mind_flay_t( this, options_str ); return player_t::create_action( name, options_str ); } From 4b79c59f489158e3c51d22234dd29d9a5244ae7f Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:27:16 -0300 Subject: [PATCH 08/42] [Priest] Shadow Word: Death --- engine/class_modules/sc_priest.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index e659f96554..d02bf1f393 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -200,6 +200,25 @@ struct mind_flay_t : public priest_spell_t } }; +// Shadow Word: Death + +// TODO: Backlash damage, effect 2 +constexpr std::array shadow_word_death_ranks { + // spell + spell_rank_t{ 1309595 }, + spell_rank_t{ 1309633 }, + spell_rank_t{ 1309635 }, + spell_rank_t{ 1309636 }, +}; +struct shadow_word_death_t : public priest_spell_t +{ + shadow_word_death_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "shadow_word_death", player, player->find_spell( player->find_rank( shadow_word_death_ranks ).spell_id ) ) + { + parse_options( options_str ); + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) @@ -212,6 +231,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new devouring_plague_t( this, options_str ); if ( name == "mind_flay" ) return new mind_flay_t( this, options_str ); + if ( name == "shadow_word_death" ) + return new shadow_word_death_t( this, options_str ); return player_t::create_action( name, options_str ); } From a176baefedcdf3149ebec760b484ddca32c54d6c Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:31:01 -0300 Subject: [PATCH 09/42] [Priest] Holy Fire --- engine/class_modules/sc_priest.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index d02bf1f393..e82a85202b 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -219,6 +219,28 @@ struct shadow_word_death_t : public priest_spell_t } }; +// Holy Fire + +constexpr std::array holy_fire_ranks { + // spell + spell_rank_t{ 14914 }, + spell_rank_t{ 15262 }, + spell_rank_t{ 15263 }, + spell_rank_t{ 15264 }, + spell_rank_t{ 15265 }, + spell_rank_t{ 15266 }, + spell_rank_t{ 15267 }, + spell_rank_t{ 15261 }, // Rank 8 is 15261 for some reason +}; +struct holy_fire_t : public priest_spell_t +{ + holy_fire_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "holy_fire", player, player->find_spell( player->find_rank( holy_fire_ranks ).spell_id ) ) + { + parse_options( options_str ); + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) @@ -233,6 +255,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new mind_flay_t( this, options_str ); if ( name == "shadow_word_death" ) return new shadow_word_death_t( this, options_str ); + if ( name == "holy_fire" ) + return new holy_fire_t( this, options_str ); return player_t::create_action( name, options_str ); } From c15b487719cd43a625101e158109e8c9fd58557f Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 20:36:10 -0300 Subject: [PATCH 10/42] [Priest] Basic APL support --- engine/class_modules/apl/priest.cpp | 14 ++++++++++++++ engine/class_modules/apl/priest.hpp | 5 +++-- engine/class_modules/sc_priest.cpp | 20 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/engine/class_modules/apl/priest.cpp b/engine/class_modules/apl/priest.cpp index 7459728bfb..7c276e9121 100644 --- a/engine/class_modules/apl/priest.cpp +++ b/engine/class_modules/apl/priest.cpp @@ -29,4 +29,18 @@ std::string temporary_enchant( const player_t* ) { return "disabled"; } + +//priest_apl_start +void priest( player_t* p ) +{ + action_priority_list_t* default_ = p->get_action_priority_list( "default" ); + + default_->add_action( "shadow_word_pain,if=!ticking" ); + default_->add_action( "devouring_plague,if=!ticking" ); + default_->add_action( "mind_blast" ); + default_->add_action( "shadow_word_death" ); + default_->add_action( "mind_flay" ); + default_->add_action( "smite" ); +} +//priest_apl_end } // namespace priest_apl diff --git a/engine/class_modules/apl/priest.hpp b/engine/class_modules/apl/priest.hpp index 00d50ba5c4..4135ad54e0 100644 --- a/engine/class_modules/apl/priest.hpp +++ b/engine/class_modules/apl/priest.hpp @@ -4,11 +4,12 @@ #include -namespace warlock_apl +namespace priest_apl { std::string potion( const player_t* ); std::string flask( const player_t* ); std::string food( const player_t* ); std::string rune( const player_t* ); std::string temporary_enchant( const player_t* ); -} // namespace warlock_apl +void priest( player_t* ); +} // namespace priest_apl diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index e82a85202b..2e56976e9d 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -6,6 +6,7 @@ #include "simulationcraft.hpp" #include "action/parse_effects.hpp" +#include "class_modules/apl/priest.hpp" namespace priestspace { @@ -19,7 +20,12 @@ struct priest_t final : public player_t void init_base_stats() override; void init_spells() override; + std::string default_potion() const override { return priest_apl::potion( this ); } + std::string default_flask() const override { return priest_apl::flask( this ); } + std::string default_food() const override { return priest_apl::food( this ); } + action_t* create_action( util::string_view name, util::string_view options_str ) override; + void init_action_list() override; struct talents_t { @@ -261,6 +267,20 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return player_t::create_action( name, options_str ); } +void priest_t::init_action_list() +{ + if ( !action_list_str.empty() ) + { + player_t::init_action_list(); + return; + } + + clear_action_priority_lists(); + priest_apl::priest( this ); + use_default_action_list = true; + player_t::init_action_list(); +} + } // namespace priestspace const module_t* module_t::priest() From 358d8ada0119e23801e383a2b5ce66391880d73b Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 23:37:13 -0300 Subject: [PATCH 11/42] [Priest] Shadowform --- engine/class_modules/sc_priest.cpp | 90 +++++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 9 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 2e56976e9d..44ee975923 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -10,15 +10,17 @@ namespace priestspace { -struct priest_t final : public player_t +struct priest_t final : public parse_player_effects_t { priest_t( sim_t* sim, util::string_view name, race_e r = RACE_NONE ) : - player_t( sim, PRIEST, name, r ) {} + parse_player_effects_t( sim, PRIEST, name, r ) {} resource_e primary_resource() const override { return RESOURCE_MANA; } void init_base_stats() override; void init_spells() override; + void create_buffs() override; + void parse_action_effects( action_t* ); std::string default_potion() const override { return priest_apl::potion( this ); } std::string default_flask() const override { return priest_apl::flask( this ); } @@ -27,9 +29,15 @@ struct priest_t final : public player_t action_t* create_action( util::string_view name, util::string_view options_str ) override; void init_action_list() override; + struct buffs_t + { + buff_t* shadowform = nullptr; + } buff; + struct talents_t { player_talent_t mind_flay; + player_talent_t shadowform; } talent; }; @@ -63,7 +71,11 @@ struct priest_action_t : public parse_action_effects_t using base_t = priest_action_t; template - priest_action_t( Args&&... args ) : ab( std::forward( args )... ) {} + priest_action_t( Args&&... args ) : ab( std::forward( args )... ) + { + if ( ab::data().ok() ) + p()->parse_action_effects( this ); + } priest_t* p() { return debug_cast( this->player ); } @@ -76,7 +88,7 @@ using priest_spell_t = priest_action_t; void priest_t::init_base_stats() { - player_t::init_base_stats(); + parse_player_effects_t::init_base_stats(); // original classic crit rates according to // * https://github.com/wowsims/classic/blob/master/sim/core/base_stats.go#L106 @@ -87,12 +99,40 @@ void priest_t::init_base_stats() void priest_t::init_spells() { - player_t::init_spells(); + parse_player_effects_t::init_spells(); parse_all_class_passives(); parse_all_passive_talents(); - talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); +} + +void priest_t::create_buffs() +{ + parse_player_effects_t::create_buffs(); + + buff.shadowform = make_buff( this, "shadowform", talent.shadowform ); + + parse_effects( buff.shadowform ); +} + +void priest_t::parse_action_effects( action_t* action ) +{ + auto _a = dynamic_cast( action ); + assert( _a ); + + _a->parse_effects( buff.shadowform ); + + // Modify Power Cost% (72) is not handled by parse_effects + const auto& shadowform_cost = buff.shadowform->data().effectN( 4 ); + if ( dbc::get_school_mask( action->get_school() ) & shadowform_cost.misc_value1() ) + { + add_parse_entry( _a->cost_effects ) + .set_buff( buff.shadowform ) + .set_value( shadowform_cost.percent() ) + .set_eff( &shadowform_cost ); + } } // Smite @@ -247,6 +287,36 @@ struct holy_fire_t : public priest_spell_t } }; +// Shadowform + +struct shadowform_t : public priest_spell_t +{ + shadowform_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "shadowform", player, player->talent.shadowform.ok() + ? player->talent.shadowform.spell() + : spell_data_t::not_found() ) + { + parse_options( options_str ); + harmful = false; + target = player; + } + + void execute() override + { + priest_spell_t::execute(); + + p()->buff.shadowform->trigger(); + } + + bool ready() override + { + if ( p()->buff.shadowform->check() ) + return false; + + return priest_spell_t::ready(); + } +}; + action_t* priest_t::create_action( util::string_view name, util::string_view options_str ) { if ( name == "smite" ) @@ -263,22 +333,24 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new shadow_word_death_t( this, options_str ); if ( name == "holy_fire" ) return new holy_fire_t( this, options_str ); + if ( name == "shadowform" ) + return new shadowform_t( this, options_str ); - return player_t::create_action( name, options_str ); + return parse_player_effects_t::create_action( name, options_str ); } void priest_t::init_action_list() { if ( !action_list_str.empty() ) { - player_t::init_action_list(); + parse_player_effects_t::init_action_list(); return; } clear_action_priority_lists(); priest_apl::priest( this ); use_default_action_list = true; - player_t::init_action_list(); + parse_player_effects_t::init_action_list(); } } // namespace priestspace From 8320d52d32373625ae7875e4b38123161b38f6e8 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 23:38:04 -0300 Subject: [PATCH 12/42] [Priest] Shadowform to pre-combat APL --- engine/class_modules/apl/priest.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/engine/class_modules/apl/priest.cpp b/engine/class_modules/apl/priest.cpp index 7c276e9121..b9021c6a01 100644 --- a/engine/class_modules/apl/priest.cpp +++ b/engine/class_modules/apl/priest.cpp @@ -33,7 +33,10 @@ std::string temporary_enchant( const player_t* ) //priest_apl_start void priest( player_t* p ) { - action_priority_list_t* default_ = p->get_action_priority_list( "default" ); + action_priority_list_t* precombat = p->get_action_priority_list( "precombat" ); + action_priority_list_t* default_ = p->get_action_priority_list( "default" ); + + precombat->add_action( "shadowform" ); default_->add_action( "shadow_word_pain,if=!ticking" ); default_->add_action( "devouring_plague,if=!ticking" ); From bfa66b412566a148347760eff283cecef5ccfe87 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 23:53:19 -0300 Subject: [PATCH 13/42] [Action] Added auto-parsing for regen inside 5sr --- engine/action/parse_effects.cpp | 21 ++++++++++++++++++++- engine/action/parse_effects.hpp | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/engine/action/parse_effects.cpp b/engine/action/parse_effects.cpp index 2b311e5109..a9b479ac1f 100644 --- a/engine/action/parse_effects.cpp +++ b/engine/action/parse_effects.cpp @@ -1087,6 +1087,19 @@ double parse_player_effects_t::stacking_movement_modifier() const return m; } +double parse_player_effects_t::resource_regen_per_second( resource_e r ) const +{ + auto reg = player_t::resource_regen_per_second( r ); + + if ( r == RESOURCE_MANA && is_player() && recent_cast() ) + { + for ( const auto& i : mana_regen_while_casting_effects ) + reg += mana_regen_from_spirit() * get_effect_value( i ); + } + + return reg; +} + void parse_player_effects_t::invalidate_cache( cache_e c ) { player_t::invalidate_cache( c ); @@ -1268,6 +1281,10 @@ std::vector* parse_player_effects_t::get_effect_vector( const s str = "absorb received"; return &absorb_received_mult_effects; + case A_MOD_MANA_REGEN_INTERRUPT: + str = "mana regen while casting"; + return &mana_regen_while_casting_effects; + case A_MOD_INCREASE_SPEED: str = "exclusive move speed"; invalidate( CACHE_RUN_SPEED ); @@ -1415,6 +1432,7 @@ void parse_player_effects_t::print_custom_parsed_effects( report::sc_html_stream print_parsed_type( os, spell_haste_effects, "Spell Haste" ); print_parsed_type( os, healing_received_effects, "Healing Received" ); print_parsed_type( os, leech_effects, "Leech" ); + print_parsed_type( os, mana_regen_while_casting_effects, "Mana Regen While Casting" ); print_parsed_type( os, non_stacking_movement_effects, "Move Speed Modifier - Exclusive" ); print_parsed_type( os, stacking_movement_effects, "Move Speed Modifier - Stacking" ); print_parsed_type( os, parry_effects, "Parry" ); @@ -1461,7 +1479,8 @@ size_t parse_player_effects_t::total_effects_count() const target_multiplier_effects.size() + target_pet_multiplier_effects.size() + non_stacking_movement_effects.size() + - stacking_movement_effects.size(); + stacking_movement_effects.size() + + mana_regen_while_casting_effects.size(); } void parse_action_base_t::parse_callback_function( pack_t& pack, parse_cb_t cb ) diff --git a/engine/action/parse_effects.hpp b/engine/action/parse_effects.hpp index ba98481f15..1fd673ea23 100644 --- a/engine/action/parse_effects.hpp +++ b/engine/action/parse_effects.hpp @@ -706,6 +706,7 @@ struct parse_player_effects_t : public player_t, public parse_effects_t std::vector target_pet_multiplier_effects; std::vector non_stacking_movement_effects; std::vector stacking_movement_effects; + std::vector mana_regen_while_casting_effects; // Cache Pairing, invalidate first of the pair when the second is invalidated std::vector> invalidate_with_parent; @@ -743,6 +744,7 @@ struct parse_player_effects_t : public player_t, public parse_effects_t bool direct ) const override; double non_stacking_movement_modifier() const override; double stacking_movement_modifier() const override; + double resource_regen_per_second( resource_e ) const override; void invalidate_cache( cache_e c ) override; From c782611f87e3910fd46ffc1f39e2797cfd5d82a4 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Mon, 19 Oct 2026 23:53:42 -0300 Subject: [PATCH 14/42] [Priest] Added support for Meditation --- engine/class_modules/sc_priest.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 44ee975923..e28e400dd8 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -36,6 +36,7 @@ struct priest_t final : public parse_player_effects_t struct talents_t { + player_talent_t meditation; player_talent_t mind_flay; player_talent_t shadowform; } talent; @@ -104,6 +105,7 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); + talent.meditation = find_talent_spell( talent_tree::CLASS, "Meditation" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); } @@ -114,6 +116,7 @@ void priest_t::create_buffs() buff.shadowform = make_buff( this, "shadowform", talent.shadowform ); + parse_effects( talent.meditation, PARSE_PASSIVE ); parse_effects( buff.shadowform ); } From 8a10ff81b388cd642f01fea75151d29decc7c85d Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 00:08:20 -0300 Subject: [PATCH 15/42] [Action] Parse effect for modify power cost based on school --- engine/action/parse_effects.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/engine/action/parse_effects.cpp b/engine/action/parse_effects.cpp index a9b479ac1f..9249dd1875 100644 --- a/engine/action/parse_effects.cpp +++ b/engine/action/parse_effects.cpp @@ -1575,6 +1575,15 @@ std::vector* parse_action_base_t::get_effect_vector( const spel return &da_multiplier_effects; } + if ( eff.subtype() == A_MOD_POWER_COST_SCHOOL_PCT ) + { + if ( !( dbc::get_school_mask( _action->get_school() ) & eff.affected_schools() ) ) + return nullptr; + + str = "cost percent"; + return &cost_effects; + } + if ( !force ) { if ( !check_affected_list( pack.affect_lists, eff, force ) ) From 9b1858d984ef1d9f497dbc3c863a6c71311311ee Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 00:08:31 -0300 Subject: [PATCH 16/42] [Priest] Simplify Shadowform implementation --- engine/class_modules/sc_priest.cpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index e28e400dd8..1db1345e69 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -126,16 +126,6 @@ void priest_t::parse_action_effects( action_t* action ) assert( _a ); _a->parse_effects( buff.shadowform ); - - // Modify Power Cost% (72) is not handled by parse_effects - const auto& shadowform_cost = buff.shadowform->data().effectN( 4 ); - if ( dbc::get_school_mask( action->get_school() ) & shadowform_cost.misc_value1() ) - { - add_parse_entry( _a->cost_effects ) - .set_buff( buff.shadowform ) - .set_value( shadowform_cost.percent() ) - .set_eff( &shadowform_cost ); - } } // Smite From ab5fcc0d48ad343adad8dcaa98e643913dc803a4 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 00:23:33 -0300 Subject: [PATCH 17/42] [Player] Added support for mana regen passive systems --- engine/player/player.cpp | 22 ++++++++++++++++++---- engine/player/player.hpp | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index fb601420ff..a0957f73c0 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -1268,6 +1268,7 @@ player_t::base_initial_current_t::base_initial_current_t() : block_value(), all_crit( 0 ), all_haste( 1.0 ), + mana_regen_while_casting( 0 ), melee_haste( 1.0 ), spell_haste( 1.0 ), ranged_haste( 1.0 ), @@ -1523,6 +1524,8 @@ void player_t::init_base_stats() base.avoidance = 0.0; base.crit_avoidance = get_passive_player_value( base.crit_avoidance, "crit_avoidance" ); + base.mana_regen_while_casting = get_passive_player_value( base.mana_regen_while_casting, "mana_regen_while_casting" ); + base.base_armor_multiplier = get_passive_player_value( base.base_armor_multiplier, "base_armor_multiplier" ); base.armor_multiplier = get_passive_player_value( base.armor_multiplier, "armor_multiplier" ); @@ -4174,7 +4177,7 @@ void player_t::min_threshold_trigger() if ( pres == RESOURCE_MANA && is_player() && recent_cast() ) { regen_delay = last_cast + 5_s - sim->current_time(); - rps = mana_regen_from_spirit(); + rps = resources.base_regen_per_second[ pres ] + mana_regen_from_spirit(); } if ( rps > 0 ) @@ -4354,9 +4357,9 @@ double player_t::resource_regen_per_second( resource_e r ) const if ( r == RESOURCE_MANA && is_player() ) { if ( recent_cast() ) - return 0; - - reg += mana_regen_from_spirit(); + reg += mana_regen_from_spirit() * current.mana_regen_while_casting; + else + reg += mana_regen_from_spirit(); } if ( resources.hasted[ r ] ) @@ -13939,12 +13942,14 @@ static constexpr std::pair field_type_map[] = { { A_MOD_SPELL_CRIT_CHANCE, "spell_crit" }, // 57 { A_MOD_DAMAGE_PERCENT_DONE, "damage_multiplier" }, // 79 { A_MOD_PERCENT_STAT, "base_attribute_multiplier" }, // 80 + { A_RESTORE_POWER, "resource_regen" }, // 85 { A_MOD_RESISTANCE_PCT, "armor_multiplier" }, // 101 { A_MOD_POWER_REGEN_PERCENT, "resource_regen" }, // 110 { A_MOD_HEALING_RECEIVED_PCT, "healing_received_multiplier" }, // 118 { A_INCREASE_RESOURCE_PCT, "resource_multiplier" }, // 119 { A_MOD_SPEED_ALWAYS, "move_speed_modifier_-_stacking" }, // 129 { A_INCREASE_RESOURCE_PCT, "resource_multiplier" }, // 132 + { A_MOD_MANA_REGEN_INTERRUPT, "mana_regen_while_casting" }, // 134 { A_MOD_HEALING_DONE_PERCENT, "healing_multiplier" }, // 136 { A_MOD_TOTAL_STAT_PERCENTAGE, "attribute_multiplier" }, // 137 { -(int)A_MOD_TOTAL_STAT_PERCENTAGE, "armor_type_multiplier" }, // -137 @@ -14289,6 +14294,15 @@ bool player_t::register_passive_effect( const spelleffect_data_t& modifying_eff, misc_type = RESOURCE_MANA; // hardcode to mana pct_val = modifying_eff.average( this ); break; + case A_RESTORE_POWER: // 85 + if ( modifying_eff.misc_value1() != POWER_MANA ) // only mana per 5 + return false; + misc_type = RESOURCE_MANA; + flat_val = modifying_eff.average( this ) / 5.0; + break; + case A_MOD_MANA_REGEN_INTERRUPT: // 134 + flat_val = modifying_eff.average( this ) * 0.01; + break; case A_MOD_TARGET_ARMOR_PCT: // 280 case A_MOD_RANGED_AND_MELEE_AUTO_ATTACK_SPEED: // 342 pct_val = -modifying_eff.average( this ); // reversed value diff --git a/engine/player/player.hpp b/engine/player/player.hpp index e1ae5fec94..6dbcf55f0c 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -227,6 +227,7 @@ struct player_t : public actor_t double hit, expertise, leech, avoidance, crit_avoidance; double spell_crit_chance, attack_crit_chance, block_value; double all_crit, all_haste; + double mana_regen_while_casting; double melee_haste, spell_haste, ranged_haste; double skill, skill_debuff, distance; double distance_to_move; From b62101aef116c54b17a7ba16167ee522e0779bc7 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 00:23:46 -0300 Subject: [PATCH 18/42] [Priest] Leave meditation to auto parser --- engine/class_modules/sc_priest.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 1db1345e69..eb17e32a38 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -36,7 +36,6 @@ struct priest_t final : public parse_player_effects_t struct talents_t { - player_talent_t meditation; player_talent_t mind_flay; player_talent_t shadowform; } talent; @@ -105,7 +104,6 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); - talent.meditation = find_talent_spell( talent_tree::CLASS, "Meditation" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); } @@ -116,7 +114,6 @@ void priest_t::create_buffs() buff.shadowform = make_buff( this, "shadowform", talent.shadowform ); - parse_effects( talent.meditation, PARSE_PASSIVE ); parse_effects( buff.shadowform ); } From c5f46795ac5e0ad3f5729e634929d1a8b137bfb2 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 00:32:32 -0300 Subject: [PATCH 19/42] [Item] Support for passive enchants that give mp5 --- engine/item/enchants.cpp | 7 +++++++ engine/player/player.cpp | 3 ++- engine/player/player.hpp | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/engine/item/enchants.cpp b/engine/item/enchants.cpp index 3ac579fa9e..d55e39d6ee 100644 --- a/engine/item/enchants.cpp +++ b/engine/item/enchants.cpp @@ -300,6 +300,13 @@ bool enchant::passive_enchant( item_t& item, unsigned spell_id ) item.parsed.enchant_notes.push_back( fmt::format( "+{}% Mana", (int)value ) ); ret = true; break; + case A_RESTORE_POWER: + if ( effect.misc_value1() != POWER_MANA ) + break; + item.player->parse_passive_item_effect( spell, effect_mask_t( false ).enable( as( i ) ) ); + item.parsed.enchant_notes.push_back( fmt::format( "+{} Mana per 5 sec", (int)value ) ); + ret = true; + break; default: break; } diff --git a/engine/player/player.cpp b/engine/player/player.cpp index a0957f73c0..afa40d0cfc 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -15452,8 +15452,9 @@ void player_t::print_parsed_effects( report::sc_html_stream& os ) const os << "\n"; } -void player_t::parse_passive_item_effect( const spell_data_t* spell ) +void player_t::parse_passive_item_effect( const spell_data_t* spell, uint32_t mask ) { + register_passive_effect_mask( spell, mask ); parse_passive_effects( spell, true, PARSE_SOURCE_ITEM ); } diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 6dbcf55f0c..8718a17b1e 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -655,7 +655,7 @@ struct player_t : public actor_t void print_parsed_effects( report::sc_html_stream& ) const; virtual void print_custom_parsed_effects( report::sc_html_stream& ) const {} - void parse_passive_item_effect( const spell_data_t* ); + void parse_passive_item_effect( const spell_data_t*, uint32_t mask = 0 ); void register_passive_item_effect_override( const spelleffect_data_t&, double ); player_t( sim_t* sim, player_e type, util::string_view name, race_e race_e ); From 94b00a62fcdab59f6a9f979bd6392dbe77b11c96 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 00:44:47 -0300 Subject: [PATCH 20/42] [Player] Fixing aura 85 parsing --- engine/player/player.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index afa40d0cfc..69413d78e5 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -14297,7 +14297,7 @@ bool player_t::register_passive_effect( const spelleffect_data_t& modifying_eff, case A_RESTORE_POWER: // 85 if ( modifying_eff.misc_value1() != POWER_MANA ) // only mana per 5 return false; - misc_type = RESOURCE_MANA; + misc_type = util::power_type_to_resource( static_cast( modifying_eff.misc_value1() ) ); flat_val = modifying_eff.average( this ) / 5.0; break; case A_MOD_MANA_REGEN_INTERRUPT: // 134 From 39a447b89483c1e709551031beec52f4f5f07417 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 19:34:07 -0300 Subject: [PATCH 21/42] [Player] Cap 5sr regen to 100% and move spirit regen call to a single place --- engine/action/parse_effects.cpp | 13 +++++-------- engine/action/parse_effects.hpp | 2 +- engine/player/player.cpp | 13 ++++++++++--- engine/player/player.hpp | 1 + 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/engine/action/parse_effects.cpp b/engine/action/parse_effects.cpp index 9249dd1875..53be0b099d 100644 --- a/engine/action/parse_effects.cpp +++ b/engine/action/parse_effects.cpp @@ -1087,17 +1087,14 @@ double parse_player_effects_t::stacking_movement_modifier() const return m; } -double parse_player_effects_t::resource_regen_per_second( resource_e r ) const +double parse_player_effects_t::composite_mana_regen_while_casting() const { - auto reg = player_t::resource_regen_per_second( r ); + auto m = player_t::composite_mana_regen_while_casting(); - if ( r == RESOURCE_MANA && is_player() && recent_cast() ) - { - for ( const auto& i : mana_regen_while_casting_effects ) - reg += mana_regen_from_spirit() * get_effect_value( i ); - } + for ( const auto& i : mana_regen_while_casting_effects ) + m += get_effect_value( i ); - return reg; + return m; } void parse_player_effects_t::invalidate_cache( cache_e c ) diff --git a/engine/action/parse_effects.hpp b/engine/action/parse_effects.hpp index 1fd673ea23..60aa88f019 100644 --- a/engine/action/parse_effects.hpp +++ b/engine/action/parse_effects.hpp @@ -744,7 +744,7 @@ struct parse_player_effects_t : public player_t, public parse_effects_t bool direct ) const override; double non_stacking_movement_modifier() const override; double stacking_movement_modifier() const override; - double resource_regen_per_second( resource_e ) const override; + double composite_mana_regen_while_casting() const override; void invalidate_cache( cache_e c ) override; diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 69413d78e5..9bff71769d 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -4356,10 +4356,12 @@ double player_t::resource_regen_per_second( resource_e r ) const if ( r == RESOURCE_MANA && is_player() ) { + double spirit_regen = mana_regen_from_spirit(); + if ( recent_cast() ) - reg += mana_regen_from_spirit() * current.mana_regen_while_casting; - else - reg += mana_regen_from_spirit(); + spirit_regen *= std::min( 1.0, composite_mana_regen_while_casting() ); + + reg += spirit_regen; } if ( resources.hasted[ r ] ) @@ -4822,6 +4824,11 @@ double player_t::composite_leech() const return current.leech + apply_combat_rating_dr( RATING_LEECH, composite_leech_rating() / current.rating.leech ); } +double player_t::composite_mana_regen_while_casting() const +{ + return current.mana_regen_while_casting; +} + double player_t::composite_run_speed() const { // speed DRs using the following formula: diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 8718a17b1e..25169f4df5 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -935,6 +935,7 @@ struct player_t : public actor_t { return 1.0; } virtual double composite_spell_hit() const; virtual double composite_leech() const; + virtual double composite_mana_regen_while_casting() const; virtual double composite_run_speed() const; virtual double composite_avoidance() const; virtual double composite_corruption() const; From 211ff2cb79ba19235775b12068279394e4dbb635 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 20:08:53 -0300 Subject: [PATCH 22/42] [Priest] Shadow Weaving --- engine/class_modules/sc_priest.cpp | 82 +++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index eb17e32a38..f55330a408 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -10,6 +10,25 @@ namespace priestspace { +struct priest_t; + +struct priest_td_t final : public actor_target_data_t +{ + struct debuffs_t + { + buff_t* shadow_weaving = nullptr; + } debuffs; + + priest_td_t( player_t* target, priest_t& p ); +}; + +// utility to create target_effect_t compatible functions from priest_td_t member references +template +static std::function d_fn( T d ) +{ + return [ d ]( actor_target_data_t* t ) { return std::invoke( d, static_cast( t )->debuffs )->check(); }; +} + struct priest_t final : public parse_player_effects_t { priest_t( sim_t* sim, util::string_view name, race_e r = RACE_NONE ) : @@ -22,6 +41,10 @@ struct priest_t final : public parse_player_effects_t void create_buffs() override; void parse_action_effects( action_t* ); + void init_special_effects() override; + + priest_td_t* get_target_data( player_t* target ) const override; + std::string default_potion() const override { return priest_apl::potion( this ); } std::string default_flask() const override { return priest_apl::flask( this ); } std::string default_food() const override { return priest_apl::food( this ); } @@ -37,10 +60,24 @@ struct priest_t final : public parse_player_effects_t struct talents_t { player_talent_t mind_flay; + player_talent_t shadow_weaving; player_talent_t shadowform; } talent; + + struct spells_t + { + const spell_data_t* shadow_weaving = spell_data_t::nil(); + } spell; + +private: + target_specific_t _target_data; }; +priest_td_t::priest_td_t( player_t* target, priest_t& p ) : actor_target_data_t( target, &p ) +{ + debuffs.shadow_weaving = make_buff( *this, "shadow_weaving", p.spell.shadow_weaving ); +} + struct priest_module_t final : public module_t { priest_module_t() : module_t( PRIEST ) {} @@ -104,8 +141,11 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); - talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); - talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); + talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); + talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); + + spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); } void priest_t::create_buffs() @@ -115,6 +155,44 @@ void priest_t::create_buffs() buff.shadowform = make_buff( this, "shadowform", talent.shadowform ); parse_effects( buff.shadowform ); + parse_target_effects( d_fn( &priest_td_t::debuffs_t::shadow_weaving ), spell.shadow_weaving ); +} + +priest_td_t* priest_t::get_target_data( player_t* target ) const +{ + priest_td_t*& td = _target_data[ target ]; + if ( !td ) + { + td = new priest_td_t( target, const_cast( *this ) ); + } + return td; +} + +void priest_t::init_special_effects() +{ + using trigger_type = dbc_proc_callback_t::trigger_fn_type; + + if ( talent.shadow_weaving.ok() ) + { + const auto driver = new special_effect_t( this ); + driver->name_str = talent.shadow_weaving->name_cstr(); + driver->spell_id = talent.shadow_weaving->id(); + driver->proc_chance_ = talent.shadow_weaving->effectN( 1 ).percent(); + special_effects.push_back( driver ); + + callbacks.register_callback_trigger_function( + driver->spell_id, trigger_type::CONDITION, []( auto, auto, auto, const action_state_t* s, auto ) { + return dbc::is_school( s->action->get_school(), SCHOOL_SHADOW ); + } ); + + callbacks.register_callback_execute_function( driver->spell_id, [ this ]( auto, auto, player_t* t, auto ) { + get_target_data( t )->debuffs.shadow_weaving->trigger(); + } ); + + new dbc_proc_callback_t( this, *driver ); + } + + parse_player_effects_t::init_special_effects(); } void priest_t::parse_action_effects( action_t* action ) From 4403d92e883b5c9e24432d7156ed976f1696fb7b Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 20:12:52 -0300 Subject: [PATCH 23/42] [Priest] Inner Focus --- engine/class_modules/sc_priest.cpp | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index f55330a408..82bb3c4a5c 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -54,11 +54,13 @@ struct priest_t final : public parse_player_effects_t struct buffs_t { + buff_t* inner_focus = nullptr; buff_t* shadowform = nullptr; } buff; struct talents_t { + player_talent_t inner_focus; player_talent_t mind_flay; player_talent_t shadow_weaving; player_talent_t shadowform; @@ -141,6 +143,7 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); + talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); @@ -152,6 +155,14 @@ void priest_t::create_buffs() { parse_player_effects_t::create_buffs(); + // Cooldown starts when the buff is consumed + buff.inner_focus = make_buff( this, "inner_focus", talent.inner_focus ) + ->set_cooldown( 0_ms ) + ->set_stack_change_callback( [ this ]( buff_t*, int, int new_ ) { + if ( !new_ ) + get_cooldown( "inner_focus" )->start( talent.inner_focus->cooldown() ); + } ); + buff.shadowform = make_buff( this, "shadowform", talent.shadowform ); parse_effects( buff.shadowform ); @@ -200,6 +211,7 @@ void priest_t::parse_action_effects( action_t* action ) auto _a = dynamic_cast( action ); assert( _a ); + _a->parse_effects( buff.inner_focus, CONSUME_BUFF ); _a->parse_effects( buff.shadowform ); } @@ -355,6 +367,37 @@ struct holy_fire_t : public priest_spell_t } }; +// Inner Focus + +struct inner_focus_t : public priest_spell_t +{ + inner_focus_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "inner_focus", player, player->talent.inner_focus.ok() + ? player->talent.inner_focus.spell() + : spell_data_t::not_found() ) + { + parse_options( options_str ); + harmful = false; + target = player; + cooldown->duration = 0_ms; + } + + void execute() override + { + priest_spell_t::execute(); + + p()->buff.inner_focus->trigger(); + } + + bool ready() override + { + if ( p()->buff.inner_focus->check() ) + return false; + + return priest_spell_t::ready(); + } +}; + // Shadowform struct shadowform_t : public priest_spell_t @@ -401,6 +444,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new shadow_word_death_t( this, options_str ); if ( name == "holy_fire" ) return new holy_fire_t( this, options_str ); + if ( name == "inner_focus" ) + return new inner_focus_t( this, options_str ); if ( name == "shadowform" ) return new shadowform_t( this, options_str ); From 70a6876be2ca5517bf90edbbbc2449fe4107c799 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 20:17:45 -0300 Subject: [PATCH 24/42] [Action] Automagically parse hit (0x16) effects --- engine/action/action.cpp | 3 +++ engine/player/player.cpp | 2 ++ 2 files changed, 5 insertions(+) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index f564665dc8..89faa37108 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -794,6 +794,9 @@ void action_t::parse_spell_data( const spell_data_t& spell_data ) base_crit_bonus += crit_bonus_mod[ 1 ]; crit_bonus_multiplier *= crit_bonus_mod[ 2 ]; + auto hit_mod = player->get_passive_value( spell_data, "hit" ); + base_hit += hit_mod[ 1 ]; + // handle mechanic modifiers (currently only bleed is found in relevant spell data) // as the effect (subtype 276) is player scoped, we call get_passive_player_value() // only the spell's mechanic field is checked, the effect's mechanic field is not sufficient for this effect subtype diff --git a/engine/player/player.cpp b/engine/player/player.cpp index c8d34adfbe..9b9226a6e5 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -13936,6 +13936,7 @@ static constexpr std::pair field_type_map[] = { { P_COOLDOWN, "cooldown" }, // 11 { P_RESOURCE_COST_1, "cost" }, // 14 { P_CRIT_BONUS, "crit_bonus" }, // 15 + { P_PENETRATION, "hit" }, // 16 { P_CHAIN_TARGETS, "chain_target" }, // 17 { P_PROC_CHANCE, "proc_chance" }, // 18 { P_TICK_TIME, "period" }, // 19 @@ -14716,6 +14717,7 @@ bool player_t::register_passive_effect( const spelleffect_data_t& modifying_eff, // action_t modifiers, these don't modify spell data case P_CRIT: // 7 case P_CRIT_BONUS: // 15 + case P_PENETRATION: // 16, "resist miss chance" (spell hit) in classic data flat_val *= 0.01; SC_FALLTHROUGH; case P_GENERIC: // 0 From 954e93443e85931672449660d52a3585fa0dfc27 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 20:33:12 -0300 Subject: [PATCH 25/42] [Priest] Remove wand specialization damage - TODO extra damage to autos --- engine/class_modules/sc_priest.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 82bb3c4a5c..85b41e5a2b 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -64,6 +64,7 @@ struct priest_t final : public parse_player_effects_t player_talent_t mind_flay; player_talent_t shadow_weaving; player_talent_t shadowform; + player_talent_t wand_specialization; } talent; struct spells_t @@ -148,6 +149,11 @@ void priest_t::init_spells() talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); + // Wand only, all schools because wand damage can be any school + // TODO: Apply to the wand auto attack once implemented + talent.wand_specialization = find_talent_spell( talent_tree::CLASS, "Wand Specialization" ); + deregister_passive_spell( talent.wand_specialization ); + spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); } From b6cfd1be33440fdab2a9a7d9faf7880a804a2c2b Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 20:52:07 -0300 Subject: [PATCH 26/42] [Priest] Early Demise --- engine/class_modules/sc_priest.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 85b41e5a2b..9a7ba8858b 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -60,6 +60,7 @@ struct priest_t final : public parse_player_effects_t struct talents_t { + player_talent_t early_demise; player_talent_t inner_focus; player_talent_t mind_flay; player_talent_t shadow_weaving; @@ -144,6 +145,7 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); + talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); @@ -348,6 +350,17 @@ struct shadow_word_death_t : public priest_spell_t priest_spell_t( "shadow_word_death", player, player->find_spell( player->find_rank( shadow_word_death_ranks ).spell_id ) ) { parse_options( options_str ); + + if ( player->talent.early_demise.ok() ) + { + const auto& crit = player->talent.early_demise->effectN( 1 ); + auto health_pct = player->talent.early_demise->effectN( 2 ).base_value(); + + add_parse_entry( target_crit_chance_effects ) + .set_func( [ health_pct ]( actor_target_data_t* td ) { return td->target->health_percentage() <= health_pct; } ) + .set_value( crit.percent() ) + .set_eff( &crit ); + } } }; From abcd0e779ed6a937fa89131963c558371c5a1f27 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 20:56:01 -0300 Subject: [PATCH 27/42] [Priest] Improved Mind Flay --- engine/class_modules/sc_priest.cpp | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 9a7ba8858b..d1e1f0f870 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -61,6 +61,7 @@ struct priest_t final : public parse_player_effects_t struct talents_t { player_talent_t early_demise; + player_talent_t improved_mind_flay; player_talent_t inner_focus; player_talent_t mind_flay; player_talent_t shadow_weaving; @@ -145,11 +146,12 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); - talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); - talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); - talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); - talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); - talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); + talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); + talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); + talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); + talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); + talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); // Wand only, all schools because wand damage can be any school // TODO: Apply to the wand auto attack once implemented @@ -331,6 +333,23 @@ struct mind_flay_t : public priest_spell_t { parse_options( options_str ); channeled = true; + + if ( player->talent.improved_mind_flay.ok() ) + update_flags |= STATE_MUL_SPELL_TA; + } + + double composite_ta_multiplier( const action_state_t* s ) const override + { + double m = priest_spell_t::composite_ta_multiplier( s ); + + if ( p()->talent.improved_mind_flay.ok() ) + { + const dot_t* dot = find_dot( s->target ); + int ticks = dot ? std::max( dot->current_tick - 1, 0 ) : 0; + m *= std::pow( 1.0 + p()->talent.improved_mind_flay->effectN( 1 ).percent(), ticks ); + } + + return m; } }; From 159c2daf686bb580434c169481c6adb313e95892 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:09:31 -0300 Subject: [PATCH 28/42] [Priest] Dark Inspirtation --- engine/class_modules/sc_priest.cpp | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index d1e1f0f870..9da691193f 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -43,6 +43,8 @@ struct priest_t final : public parse_player_effects_t void init_special_effects() override; + double composite_attribute_multiplier( attribute_e ) const override; + priest_td_t* get_target_data( player_t* target ) const override; std::string default_potion() const override { return priest_apl::potion( this ); } @@ -54,12 +56,14 @@ struct priest_t final : public parse_player_effects_t struct buffs_t { + buff_t* dark_inspiration = nullptr; buff_t* inner_focus = nullptr; buff_t* shadowform = nullptr; } buff; struct talents_t { + player_talent_t dark_inspiration; player_talent_t early_demise; player_talent_t improved_mind_flay; player_talent_t inner_focus; @@ -71,6 +75,7 @@ struct priest_t final : public parse_player_effects_t struct spells_t { + const spell_data_t* dark_inspiration = spell_data_t::nil(); const spell_data_t* shadow_weaving = spell_data_t::nil(); } spell; @@ -146,6 +151,7 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); + talent.dark_inspiration = find_talent_spell( talent_tree::CLASS, "Dark Inspiration" ); talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); @@ -158,13 +164,18 @@ void priest_t::init_spells() talent.wand_specialization = find_talent_spell( talent_tree::CLASS, "Wand Specialization" ); deregister_passive_spell( talent.wand_specialization ); - spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); + spell.dark_inspiration = find_spell( 1326168 ); + spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); } void priest_t::create_buffs() { parse_player_effects_t::create_buffs(); + buff.dark_inspiration = make_buff( this, "dark_inspiration", spell.dark_inspiration ) + ->set_default_value( talent.dark_inspiration->effectN( 1 ).percent() ) + ->add_invalidate( CACHE_SPIRIT ); + // Cooldown starts when the buff is consumed buff.inner_focus = make_buff( this, "inner_focus", talent.inner_focus ) ->set_cooldown( 0_ms ) @@ -216,6 +227,17 @@ void priest_t::init_special_effects() parse_player_effects_t::init_special_effects(); } +// parse_effects does not parse party buffs with range TODO improve core to allow that? +double priest_t::composite_attribute_multiplier( attribute_e attr ) const +{ + double m = parse_player_effects_t::composite_attribute_multiplier( attr ); + + if ( attr == ATTR_SPIRIT ) + m *= 1.0 + buff.dark_inspiration->check_value(); + + return m; +} + void priest_t::parse_action_effects( action_t* action ) { auto _a = dynamic_cast( action ); @@ -290,6 +312,14 @@ struct mind_blast_t : public priest_spell_t { parse_options( options_str ); } + + void execute() override + { + priest_spell_t::execute(); + + if ( p()->talent.dark_inspiration.ok() ) + p()->buff.dark_inspiration->trigger(); + } }; // Devouring Plague @@ -351,6 +381,14 @@ struct mind_flay_t : public priest_spell_t return m; } + + void last_tick( dot_t* d ) override + { + priest_spell_t::last_tick( d ); + + if ( p()->talent.dark_inspiration.ok() && d->current_tick == d->num_ticks() ) + p()->buff.dark_inspiration->trigger(); + } }; // Shadow Word: Death From b20e4e32821620b3f898fb7081e461d74e5a2fc9 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:19:30 -0300 Subject: [PATCH 29/42] [Player] Support for spell power from Spirit from talents --- engine/player/player.cpp | 19 ++++++++++++++++++- engine/player/player.hpp | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 9b9226a6e5..d7c40e4a1c 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -1243,6 +1243,7 @@ player_t::~player_t() = default; player_t::base_initial_current_t::base_initial_current_t() : stats(), spell_power_per_intellect( 0 ), + spell_power_per_spirit( 0 ), spell_power_per_attack_power( 0 ), attack_power_per_strength( 0 ), attack_power_per_agility( 0 ), @@ -1315,6 +1316,7 @@ void sc_format_to( const player_t::base_initial_current_t& s, fmt::format_contex { fmt::format_to( out, "{}", s.stats ); fmt::format_to( out, " spell_power_per_intellect={:.6g}", s.spell_power_per_intellect ); + fmt::format_to( out, " spell_power_per_spirit={:.6g}", s.spell_power_per_spirit ); fmt::format_to( out, " spell_power_per_attack_power={:.6g}", s.spell_power_per_attack_power ); fmt::format_to( out, " attack_power_per_strength={:.6g}", s.attack_power_per_strength ); fmt::format_to( out, " attack_power_per_agility={:.6g}", s.attack_power_per_agility ); @@ -1526,6 +1528,9 @@ void player_t::init_base_stats() base.mana_regen_while_casting = get_passive_player_value( base.mana_regen_while_casting, "mana_regen_while_casting" ); + base.spell_power_per_spirit = + get_passive_player_value( base.spell_power_per_spirit, "spell_power_from_attribute", ATTR_SPIRIT ); + base.base_armor_multiplier = get_passive_player_value( base.base_armor_multiplier, "base_armor_multiplier" ); base.armor_multiplier = get_passive_player_value( base.armor_multiplier, "armor_multiplier" ); @@ -4759,6 +4764,7 @@ double player_t::composite_spell_power( school_e /* school */ ) const double sp = current.stats.spell_power; sp += current.spell_power_per_intellect * cache.intellect(); + sp += current.spell_power_per_spirit * cache.spirit(); return sp; } @@ -5235,6 +5241,11 @@ void player_t::invalidate_cache( cache_e c ) invalidate_cache( CACHE_SPELL_CRIT_CHANCE ); break; + case CACHE_SPIRIT: + if ( current.spell_power_per_spirit > 0 ) + invalidate_cache( CACHE_SPELL_POWER ); + break; + case CACHE_SPELL_POWER: if ( current.attack_power_per_spell_power > 0 ) invalidate_cache( CACHE_ATTACK_POWER ); @@ -13973,6 +13984,7 @@ static constexpr std::pair field_type_map[] = { { A_MOD_CRIT_DAMAGE_MULTIPLIER, "crit_damage_multiplier" }, // 163 { A_MOD_ATTACK_POWER_PCT, "attack_power_multiplier", }, // 166 { A_MOD_RECHARGE_TIME_PCT_CATEGORY_MASK, "charge_cooldown" }, // 173 + { A_MOD_SPELL_DAMAGE_OF_STAT_PERCENT, "spell_power_from_attribute" }, // 174 { A_MOD_MAX_MANA_PCT, "resource_multiplier" }, // 178 { A_MOD_ATTACKER_MELEE_CRIT_CHANCE, "crit_avoidance" }, // 187 { A_HASTE_ALL, "all_haste" }, // 193 @@ -14052,8 +14064,9 @@ struct misc_expansion_t std::function formatter; }; -static const std::array misc_expansion_map = { { +static const std::array misc_expansion_map = { { { "attribute_value", MISC_LIST_ATTR, &attr_formatter }, + { "spell_power_from_attribute", MISC_LIST_ATTR, &attr_formatter }, { "base_attribute_multiplier", MISC_LIST_ATTR, &attr_formatter }, { "attribute_multiplier", MISC_BITMAP_ATTR, &attr_formatter }, { "armor_type_multiplier", MISC_BITMAP_ATTR, &attr_formatter }, @@ -14319,6 +14332,10 @@ bool player_t::register_passive_effect( const spelleffect_data_t& modifying_eff, case A_MOD_MANA_REGEN_INTERRUPT: // 134 flat_val = modifying_eff.average( this ) * 0.01; break; + case A_MOD_SPELL_DAMAGE_OF_STAT_PERCENT: // 174 + misc_type = modifying_eff.misc_value2(); // Stat type is in misc_value2 + flat_val = modifying_eff.average( this ) * 0.01; + break; case A_MOD_TARGET_ARMOR_PCT: // 280 case A_MOD_RANGED_AND_MELEE_AUTO_ATTACK_SPEED: // 342 pct_val = -modifying_eff.average( this ); // reversed value diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 25169f4df5..cfbeed386e 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -217,7 +217,7 @@ struct player_t : public actor_t gear_stats_t stats; - double spell_power_per_intellect, spell_power_per_attack_power; + double spell_power_per_intellect, spell_power_per_spirit, spell_power_per_attack_power; double attack_power_per_strength, attack_power_per_agility, attack_power_per_spell_power; double dodge_per_agility, parry_per_strength, parry_rating_per_crit_rating; double attack_crit_per_agility, spell_crit_per_intellect; From 1577245966c51aa421bae116db941ea437f18a37 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:24:45 -0300 Subject: [PATCH 30/42] [Priest] Devouring contagion jumps --- engine/class_modules/sc_priest.cpp | 57 ++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 9da691193f..9258824df1 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -14,6 +14,11 @@ struct priest_t; struct priest_td_t final : public actor_target_data_t { + struct dots_t + { + dot_t* devouring_plague = nullptr; + } dots; + struct debuffs_t { buff_t* shadow_weaving = nullptr; @@ -42,6 +47,7 @@ struct priest_t final : public parse_player_effects_t void parse_action_effects( action_t* ); void init_special_effects() override; + void activate() override; double composite_attribute_multiplier( attribute_e ) const override; @@ -64,6 +70,7 @@ struct priest_t final : public parse_player_effects_t struct talents_t { player_talent_t dark_inspiration; + player_talent_t devouring_contagion; player_talent_t early_demise; player_talent_t improved_mind_flay; player_talent_t inner_focus; @@ -85,6 +92,8 @@ struct priest_t final : public parse_player_effects_t priest_td_t::priest_td_t( player_t* target, priest_t& p ) : actor_target_data_t( target, &p ) { + dots.devouring_plague = target->get_dot( "devouring_plague", &p ); + debuffs.shadow_weaving = make_buff( *this, "shadow_weaving", p.spell.shadow_weaving ); } @@ -151,13 +160,14 @@ void priest_t::init_spells() parse_all_class_passives(); parse_all_passive_talents(); - talent.dark_inspiration = find_talent_spell( talent_tree::CLASS, "Dark Inspiration" ); - talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); - talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); - talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); - talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); - talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); - talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); + talent.dark_inspiration = find_talent_spell( talent_tree::CLASS, "Dark Inspiration" ); + talent.devouring_contagion = find_talent_spell( talent_tree::CLASS, "Devouring Contagion" ); + talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); + talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); + talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); + talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); + talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); // Wand only, all schools because wand damage can be any school // TODO: Apply to the wand auto attack once implemented @@ -227,6 +237,39 @@ void priest_t::init_special_effects() parse_player_effects_t::init_special_effects(); } +void priest_t::activate() +{ + parse_player_effects_t::activate(); + + if ( talent.devouring_contagion.ok() ) + { + auto range = talent.devouring_contagion->effectN( 2 ).base_value(); + auto targets = as( talent.devouring_contagion->effectN( 3 ).base_value() ); + + register_on_kill_callback( [ this, range, targets ]( player_t* t ) { + if ( sim->event_mgr.canceled ) + return; + + auto source = get_target_data( t )->dots.devouring_plague; + if ( !source->is_ticking() ) + return; + + auto tl = sim->target_non_sleeping_list.data(); + if ( sim->distance_targeting_enabled ) + range::erase_remove( tl, [ t, range ]( player_t* e ) { return e->get_player_distance( *t ) > range; } ); + + // prioritize undotted over dotted + rng().shuffle( tl.begin(), tl.end() ); + std::stable_partition( tl.begin(), tl.end(), [ this ]( player_t* e ) { + return !get_target_data( e )->dots.devouring_plague->is_ticking(); + } ); + + for ( size_t i = 0; i < std::min( targets, tl.size() ); i++ ) + source->copy( tl[ i ], DOT_COPY_START ); + } ); + } +} + // parse_effects does not parse party buffs with range TODO improve core to allow that? double priest_t::composite_attribute_multiplier( attribute_e attr ) const { From 65de9ceb7f296b1e9abe5939f35af08a7aaf47de Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:28:25 -0300 Subject: [PATCH 31/42] [Priest] Power in the Light --- engine/class_modules/sc_priest.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 9258824df1..3c60ccbb4f 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -17,6 +17,8 @@ struct priest_td_t final : public actor_target_data_t struct dots_t { dot_t* devouring_plague = nullptr; + dot_t* holy_fire = nullptr; + dot_t* shadow_word_pain = nullptr; } dots; struct debuffs_t @@ -75,6 +77,7 @@ struct priest_t final : public parse_player_effects_t player_talent_t improved_mind_flay; player_talent_t inner_focus; player_talent_t mind_flay; + player_talent_t power_in_light; player_talent_t shadow_weaving; player_talent_t shadowform; player_talent_t wand_specialization; @@ -93,6 +96,8 @@ struct priest_t final : public parse_player_effects_t priest_td_t::priest_td_t( player_t* target, priest_t& p ) : actor_target_data_t( target, &p ) { dots.devouring_plague = target->get_dot( "devouring_plague", &p ); + dots.holy_fire = target->get_dot( "holy_fire", &p ); + dots.shadow_word_pain = target->get_dot( "shadow_word_pain", &p ); debuffs.shadow_weaving = make_buff( *this, "shadow_weaving", p.spell.shadow_weaving ); } @@ -166,6 +171,7 @@ void priest_t::init_spells() talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.power_in_light = find_talent_spell( talent_tree::CLASS, "Power in Light" ); talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); @@ -309,6 +315,20 @@ struct smite_t : public priest_spell_t priest_spell_t( "smite", player, player->find_spell( player->find_rank( smite_ranks ).spell_id ) ) { parse_options( options_str ); + + // TODO: Also applies to Penance once implemented + if ( player->talent.power_in_light.ok() ) + { + const auto& eff = player->talent.power_in_light->effectN( 1 ); + + add_parse_entry( target_multiplier_effects ) + .set_func( []( actor_target_data_t* td ) { + const auto& dots = static_cast( td )->dots; + return dots.holy_fire->is_ticking() || dots.shadow_word_pain->is_ticking(); + } ) + .set_value( eff.percent() ) + .set_eff( &eff ); + } } }; From a255f029202307479237f2875a0ba4578a10bb27 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:33:07 -0300 Subject: [PATCH 32/42] [Priest] Penance --- engine/class_modules/sc_priest.cpp | 77 ++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 3c60ccbb4f..2b58dc01ba 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -77,6 +77,7 @@ struct priest_t final : public parse_player_effects_t player_talent_t improved_mind_flay; player_talent_t inner_focus; player_talent_t mind_flay; + player_talent_t penance; player_talent_t power_in_light; player_talent_t shadow_weaving; player_talent_t shadowform; @@ -143,6 +144,22 @@ struct priest_action_t : public parse_action_effects_t const priest_t* p() const { return debug_cast( this->player ); } + + void parse_power_in_light() + { + if ( !p()->talent.power_in_light.ok() ) + return; + + const auto& eff = p()->talent.power_in_light->effectN( 1 ); + + add_parse_entry( this->target_multiplier_effects ) + .set_func( []( actor_target_data_t* td ) { + const auto& dots = static_cast( td )->dots; + return dots.holy_fire->is_ticking() || dots.shadow_word_pain->is_ticking(); + } ) + .set_value( eff.percent() ) + .set_eff( &eff ); + } }; using priest_spell_t = priest_action_t; @@ -171,6 +188,7 @@ void priest_t::init_spells() talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); + talent.penance = find_talent_spell( talent_tree::CLASS, "Penance" ); talent.power_in_light = find_talent_spell( talent_tree::CLASS, "Power in Light" ); talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); @@ -315,20 +333,7 @@ struct smite_t : public priest_spell_t priest_spell_t( "smite", player, player->find_spell( player->find_rank( smite_ranks ).spell_id ) ) { parse_options( options_str ); - - // TODO: Also applies to Penance once implemented - if ( player->talent.power_in_light.ok() ) - { - const auto& eff = player->talent.power_in_light->effectN( 1 ); - - add_parse_entry( target_multiplier_effects ) - .set_func( []( actor_target_data_t* td ) { - const auto& dots = static_cast( td )->dots; - return dots.holy_fire->is_ticking() || dots.shadow_word_pain->is_ticking(); - } ) - .set_value( eff.percent() ) - .set_eff( &eff ); - } + parse_power_in_light(); } }; @@ -506,6 +511,48 @@ struct holy_fire_t : public priest_spell_t } }; +// Penance + +struct penance_rank_t +{ + unsigned spell_id; // Penance + unsigned channel_id; // Penance (channel) + unsigned damage_id; // Penance (damage) +}; +constexpr std::array penance_ranks { + // spell channel damage + penance_rank_t{ 402174, 402261, 402284 }, + penance_rank_t{ 1240720, 1240734, 1240727 }, + penance_rank_t{ 1240721, 1240736, 1240730 }, + penance_rank_t{ 1316995, 1316994, 1316993 }, +}; + +struct penance_damage_t : public priest_spell_t +{ + penance_damage_t( priest_t* player ) : + priest_spell_t( "penance_damage", player, player->find_spell( player->find_rank( penance_ranks ).damage_id ) ) + { + background = true; + parse_power_in_light(); + } +}; + +struct penance_t : public priest_spell_t +{ + penance_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "penance", player, player->talent.penance.ok() + ? player->find_spell( player->find_rank( penance_ranks ).spell_id ) + : spell_data_t::not_found() ) + { + parse_options( options_str ); + channeled = true; + may_miss = may_crit = false; + + parse_effect_data( player->find_spell( player->find_rank( penance_ranks ).channel_id )->effectN( 2 ) ); + tick_action = new penance_damage_t( player ); + } +}; + // Inner Focus struct inner_focus_t : public priest_spell_t @@ -583,6 +630,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new shadow_word_death_t( this, options_str ); if ( name == "holy_fire" ) return new holy_fire_t( this, options_str ); + if ( name == "penance" ) + return new penance_t( this, options_str ); if ( name == "inner_focus" ) return new inner_focus_t( this, options_str ); if ( name == "shadowform" ) From e8f1435c8282f5af8138891bd385c17da0bf76ab Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:33:56 -0300 Subject: [PATCH 33/42] [Priest] Added comment about penance issues with Inner Focus --- engine/class_modules/sc_priest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 2b58dc01ba..7977901e74 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -511,7 +511,7 @@ struct holy_fire_t : public priest_spell_t } }; -// Penance +// Penance TODO: Check Penance vs Inner Focus - Right now only first bolt gets the crit bonus struct penance_rank_t { From 1f88319037c7094158fc61b3f0c6cbf601b79172 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:35:47 -0300 Subject: [PATCH 34/42] [Priest] Holy Nova --- engine/class_modules/sc_priest.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 7977901e74..d281ea8d6a 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -74,6 +74,7 @@ struct priest_t final : public parse_player_effects_t player_talent_t dark_inspiration; player_talent_t devouring_contagion; player_talent_t early_demise; + player_talent_t holy_nova; player_talent_t improved_mind_flay; player_talent_t inner_focus; player_talent_t mind_flay; @@ -185,6 +186,7 @@ void priest_t::init_spells() talent.dark_inspiration = find_talent_spell( talent_tree::CLASS, "Dark Inspiration" ); talent.devouring_contagion = find_talent_spell( talent_tree::CLASS, "Devouring Contagion" ); talent.early_demise = find_talent_spell( talent_tree::CLASS, "Early Demise" ); + talent.holy_nova = find_talent_spell( talent_tree::CLASS, "Holy Nova" ); talent.improved_mind_flay = find_talent_spell( talent_tree::CLASS, "Improved Mind Flay" ); talent.inner_focus = find_talent_spell( talent_tree::CLASS, "Inner Focus" ); talent.mind_flay = find_talent_spell( talent_tree::CLASS, "Mind Flay" ); @@ -553,6 +555,30 @@ struct penance_t : public priest_spell_t } }; +// Holy Nova + +// TODO: Party heal +constexpr std::array holy_nova_ranks { + // spell + spell_rank_t{ 15237 }, + spell_rank_t{ 15430 }, + spell_rank_t{ 15431 }, + spell_rank_t{ 27799 }, + spell_rank_t{ 27800 }, + spell_rank_t{ 27801 }, +}; +struct holy_nova_t : public priest_spell_t +{ + holy_nova_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "holy_nova", player, player->talent.holy_nova.ok() + ? player->find_spell( player->find_rank( holy_nova_ranks ).spell_id ) + : spell_data_t::not_found() ) + { + parse_options( options_str ); + aoe = -1; + } +}; + // Inner Focus struct inner_focus_t : public priest_spell_t @@ -632,6 +658,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new holy_fire_t( this, options_str ); if ( name == "penance" ) return new penance_t( this, options_str ); + if ( name == "holy_nova" ) + return new holy_nova_t( this, options_str ); if ( name == "inner_focus" ) return new inner_focus_t( this, options_str ); if ( name == "shadowform" ) From 56a6108be6874926aded562510ddc27e110fe4ef Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:38:12 -0300 Subject: [PATCH 35/42] [Priest] Searing Light --- engine/class_modules/sc_priest.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index d281ea8d6a..d886357133 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -65,6 +65,7 @@ struct priest_t final : public parse_player_effects_t struct buffs_t { buff_t* dark_inspiration = nullptr; + buff_t* holy_purpose = nullptr; buff_t* inner_focus = nullptr; buff_t* shadowform = nullptr; } buff; @@ -80,6 +81,7 @@ struct priest_t final : public parse_player_effects_t player_talent_t mind_flay; player_talent_t penance; player_talent_t power_in_light; + player_talent_t searing_light; player_talent_t shadow_weaving; player_talent_t shadowform; player_talent_t wand_specialization; @@ -88,6 +90,7 @@ struct priest_t final : public parse_player_effects_t struct spells_t { const spell_data_t* dark_inspiration = spell_data_t::nil(); + const spell_data_t* holy_purpose = spell_data_t::nil(); const spell_data_t* shadow_weaving = spell_data_t::nil(); } spell; @@ -193,6 +196,7 @@ void priest_t::init_spells() talent.penance = find_talent_spell( talent_tree::CLASS, "Penance" ); talent.power_in_light = find_talent_spell( talent_tree::CLASS, "Power in Light" ); talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); + talent.searing_light = find_talent_spell( talent_tree::CLASS, "Searing Light" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); // Wand only, all schools because wand damage can be any school @@ -201,6 +205,7 @@ void priest_t::init_spells() deregister_passive_spell( talent.wand_specialization ); spell.dark_inspiration = find_spell( 1326168 ); + spell.holy_purpose = talent.searing_light->effectN( 2 ).trigger(); spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); } @@ -212,6 +217,8 @@ void priest_t::create_buffs() ->set_default_value( talent.dark_inspiration->effectN( 1 ).percent() ) ->add_invalidate( CACHE_SPIRIT ); + buff.holy_purpose = make_buff( this, "holy_purpose", spell.holy_purpose ); + // Cooldown starts when the buff is consumed buff.inner_focus = make_buff( this, "inner_focus", talent.inner_focus ) ->set_cooldown( 0_ms ) @@ -260,6 +267,27 @@ void priest_t::init_special_effects() new dbc_proc_callback_t( this, *driver ); } + if ( talent.searing_light.ok() ) + { + const auto driver = new special_effect_t( this ); + driver->name_str = talent.searing_light->name_cstr(); + driver->spell_id = talent.searing_light->id(); + driver->proc_chance_ = talent.searing_light->effectN( 2 ).percent(); + special_effects.push_back( driver ); + + callbacks.register_callback_trigger_function( + driver->spell_id, trigger_type::CONDITION, [ this ]( auto, auto, auto, const action_state_t* s, auto ) { + return s->result_type == result_amount_type::DMG_OVER_TIME && + s->action->get_dot( s->target ) == get_target_data( s->target )->dots.holy_fire; + } ); + + callbacks.register_callback_execute_function( driver->spell_id, [ this ]( auto, auto, auto, auto ) { + buff.holy_purpose->trigger(); + } ); + + new dbc_proc_callback_t( this, *driver ); + } + parse_player_effects_t::init_special_effects(); } @@ -312,6 +340,7 @@ void priest_t::parse_action_effects( action_t* action ) auto _a = dynamic_cast( action ); assert( _a ); + _a->parse_effects( buff.holy_purpose, CONSUME_BUFF ); _a->parse_effects( buff.inner_focus, CONSUME_BUFF ); _a->parse_effects( buff.shadowform ); } From 5386ab0f277fddeea5655d71820df5cee58b86c8 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:42:35 -0300 Subject: [PATCH 36/42] [Priest] Starshards --- engine/class_modules/sc_priest.cpp | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index d886357133..d49a8abd48 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -55,6 +55,14 @@ struct priest_t final : public parse_player_effects_t priest_td_t* get_target_data( player_t* target ) const override; + // Priest racials are not in the racial spell table, so check the spell's race mask instead + template + const spell_data_t* find_racial_rank( const RANKS& ranks ) const + { + auto spell = find_spell( find_rank( ranks ).spell_id ); + return spell->race_mask() & util::race_mask( race ) ? spell : spell_data_t::not_found(); + } + std::string default_potion() const override { return priest_apl::potion( this ); } std::string default_flask() const override { return priest_apl::flask( this ); } std::string default_food() const override { return priest_apl::food( this ); } @@ -608,6 +616,28 @@ struct holy_nova_t : public priest_spell_t } }; +// Starshards + +constexpr std::array starshards_ranks { + // spell + spell_rank_t{ 10797 }, + spell_rank_t{ 19296 }, + spell_rank_t{ 19299 }, + spell_rank_t{ 19302 }, + spell_rank_t{ 19303 }, + spell_rank_t{ 19304 }, + spell_rank_t{ 19305 }, +}; +struct starshards_t : public priest_spell_t +{ + starshards_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "starshards", player, player->find_racial_rank( starshards_ranks ) ) + { + parse_options( options_str ); + channeled = true; + } +}; + // Inner Focus struct inner_focus_t : public priest_spell_t @@ -689,6 +719,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new penance_t( this, options_str ); if ( name == "holy_nova" ) return new holy_nova_t( this, options_str ); + if ( name == "starshards" ) + return new starshards_t( this, options_str ); if ( name == "inner_focus" ) return new inner_focus_t( this, options_str ); if ( name == "shadowform" ) From 952898011fec8873a8c1c78e6c8daf436e02c11e Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:46:42 -0300 Subject: [PATCH 37/42] [Priest] Add penance and starshards to APL --- engine/class_modules/apl/priest.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/engine/class_modules/apl/priest.cpp b/engine/class_modules/apl/priest.cpp index b9021c6a01..49a93c2f3c 100644 --- a/engine/class_modules/apl/priest.cpp +++ b/engine/class_modules/apl/priest.cpp @@ -42,6 +42,8 @@ void priest( player_t* p ) default_->add_action( "devouring_plague,if=!ticking" ); default_->add_action( "mind_blast" ); default_->add_action( "shadow_word_death" ); + default_->add_action( "penance" ); + default_->add_action( "starshards" ); default_->add_action( "mind_flay" ); default_->add_action( "smite" ); } From c35b6779b90c6f065a4016585b368834e43f5aa4 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:48:35 -0300 Subject: [PATCH 38/42] [Priest] Dark Sacrifice --- engine/class_modules/sc_priest.cpp | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index d49a8abd48..8dc515d1b9 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -638,6 +638,35 @@ struct starshards_t : public priest_spell_t } }; +// Dark Sacrifice + +// TODO: Health cost, effect 1 +constexpr std::array dark_sacrifice_ranks { + // spell + spell_rank_t{ 1277324 }, + spell_rank_t{ 1277325 }, + spell_rank_t{ 1277326 }, + spell_rank_t{ 1277327 }, + spell_rank_t{ 1277328 }, +}; +struct dark_sacrifice_t : public priest_spell_t +{ + dark_sacrifice_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "dark_sacrifice", player, player->find_racial_rank( dark_sacrifice_ranks ) ) + { + parse_options( options_str ); + harmful = false; + target = player; + } + + void execute() override + { + priest_spell_t::execute(); + + player->resource_gain( RESOURCE_MANA, player->cache.spirit(), gain, this ); + } +}; + // Inner Focus struct inner_focus_t : public priest_spell_t @@ -721,6 +750,8 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new holy_nova_t( this, options_str ); if ( name == "starshards" ) return new starshards_t( this, options_str ); + if ( name == "dark_sacrifice" ) + return new dark_sacrifice_t( this, options_str ); if ( name == "inner_focus" ) return new inner_focus_t( this, options_str ); if ( name == "shadowform" ) From 7f0bcd72514dc418c750c61153c9dc4005e78621 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Tue, 20 Oct 2026 21:49:23 -0300 Subject: [PATCH 39/42] [Priest] Dark Sacrifice APL --- engine/class_modules/apl/priest.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/engine/class_modules/apl/priest.cpp b/engine/class_modules/apl/priest.cpp index 49a93c2f3c..1f271b159d 100644 --- a/engine/class_modules/apl/priest.cpp +++ b/engine/class_modules/apl/priest.cpp @@ -38,6 +38,7 @@ void priest( player_t* p ) precombat->add_action( "shadowform" ); + default_->add_action( "dark_sacrifice,if=mana.pct<50" ); default_->add_action( "shadow_word_pain,if=!ticking" ); default_->add_action( "devouring_plague,if=!ticking" ); default_->add_action( "mind_blast" ); From c05d97c7cfd3a342417f3a7349e53014906f86f0 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Sat, 24 Oct 2026 13:32:55 -0300 Subject: [PATCH 40/42] [Priest] Adapting to new spell ranks --- engine/class_modules/sc_priest.cpp | 187 +++++++++++++---------------- engine/player/player.hpp | 17 --- 2 files changed, 85 insertions(+), 119 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 8dc515d1b9..7785aef116 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -56,10 +56,9 @@ struct priest_t final : public parse_player_effects_t priest_td_t* get_target_data( player_t* target ) const override; // Priest racials are not in the racial spell table, so check the spell's race mask instead - template - const spell_data_t* find_racial_rank( const RANKS& ranks ) const + const spell_data_t* find_racial_rank( util::span ranks ) const { - auto spell = find_spell( find_rank( ranks ).spell_id ); + auto spell = find_rank( ranks )->spell( this ); return spell->race_mask() & util::race_mask( race ) ? spell : spell_data_t::not_found(); } @@ -356,20 +355,19 @@ void priest_t::parse_action_effects( action_t* action ) // Smite constexpr std::array smite_ranks { - // spell - spell_rank_t{ 585 }, - spell_rank_t{ 591 }, - spell_rank_t{ 598 }, - spell_rank_t{ 984 }, - spell_rank_t{ 1004 }, - spell_rank_t{ 6060 }, - spell_rank_t{ 10933 }, - spell_rank_t{ 10934 }, + spell_container_t().set_spell( 585 ), + spell_container_t().set_spell( 591 ), + spell_container_t().set_spell( 598 ), + spell_container_t().set_spell( 984 ), + spell_container_t().set_spell( 1004 ), + spell_container_t().set_spell( 6060 ), + spell_container_t().set_spell( 10933 ), + spell_container_t().set_spell( 10934 ), }; struct smite_t : public priest_spell_t { smite_t( priest_t* player, util::string_view options_str ) : - priest_spell_t( "smite", player, player->find_spell( player->find_rank( smite_ranks ).spell_id ) ) + priest_spell_t( "smite", player, player->find_rank( smite_ranks )->spell( player ) ) { parse_options( options_str ); parse_power_in_light(); @@ -379,20 +377,19 @@ struct smite_t : public priest_spell_t // Shadow Word: Pain constexpr std::array shadow_word_pain_ranks { - // spell - spell_rank_t{ 589 }, - spell_rank_t{ 594 }, - spell_rank_t{ 970 }, - spell_rank_t{ 992 }, - spell_rank_t{ 2767 }, - spell_rank_t{ 10892 }, - spell_rank_t{ 10893 }, - spell_rank_t{ 10894 }, + spell_container_t().set_spell( 589 ), + spell_container_t().set_spell( 594 ), + spell_container_t().set_spell( 970 ), + spell_container_t().set_spell( 992 ), + spell_container_t().set_spell( 2767 ), + spell_container_t().set_spell( 10892 ), + spell_container_t().set_spell( 10893 ), + spell_container_t().set_spell( 10894 ), }; struct shadow_word_pain_t : public priest_spell_t { shadow_word_pain_t( priest_t* player, util::string_view options_str ) : - priest_spell_t( "shadow_word_pain", player, player->find_spell( player->find_rank( shadow_word_pain_ranks ).spell_id ) ) + priest_spell_t( "shadow_word_pain", player, player->find_rank( shadow_word_pain_ranks )->spell( player ) ) { parse_options( options_str ); } @@ -401,21 +398,20 @@ struct shadow_word_pain_t : public priest_spell_t // Mind Blast constexpr std::array mind_blast_ranks { - // spell - spell_rank_t{ 8092 }, - spell_rank_t{ 8102 }, - spell_rank_t{ 8103 }, - spell_rank_t{ 8104 }, - spell_rank_t{ 8105 }, - spell_rank_t{ 8106 }, - spell_rank_t{ 10945 }, - spell_rank_t{ 10946 }, - spell_rank_t{ 10947 }, + spell_container_t().set_spell( 8092 ), + spell_container_t().set_spell( 8102 ), + spell_container_t().set_spell( 8103 ), + spell_container_t().set_spell( 8104 ), + spell_container_t().set_spell( 8105 ), + spell_container_t().set_spell( 8106 ), + spell_container_t().set_spell( 10945 ), + spell_container_t().set_spell( 10946 ), + spell_container_t().set_spell( 10947 ), }; struct mind_blast_t : public priest_spell_t { mind_blast_t( priest_t* player, util::string_view options_str ) : - priest_spell_t( "mind_blast", player, player->find_spell( player->find_rank( mind_blast_ranks ).spell_id ) ) + priest_spell_t( "mind_blast", player, player->find_rank( mind_blast_ranks )->spell( player ) ) { parse_options( options_str ); } @@ -433,18 +429,17 @@ struct mind_blast_t : public priest_spell_t // TODO: Heal the caster for the damage dealt constexpr std::array devouring_plague_ranks { - // spell - spell_rank_t{ 2944 }, - spell_rank_t{ 19276 }, - spell_rank_t{ 19277 }, - spell_rank_t{ 19278 }, - spell_rank_t{ 19279 }, - spell_rank_t{ 19280 }, + spell_container_t().set_spell( 2944 ), + spell_container_t().set_spell( 19276 ), + spell_container_t().set_spell( 19277 ), + spell_container_t().set_spell( 19278 ), + spell_container_t().set_spell( 19279 ), + spell_container_t().set_spell( 19280 ), }; struct devouring_plague_t : public priest_spell_t { devouring_plague_t( priest_t* player, util::string_view options_str ) : - priest_spell_t( "devouring_plague", player, player->find_spell( player->find_rank( devouring_plague_ranks ).spell_id ) ) + priest_spell_t( "devouring_plague", player, player->find_rank( devouring_plague_ranks )->spell( player ) ) { parse_options( options_str ); } @@ -453,19 +448,18 @@ struct devouring_plague_t : public priest_spell_t // Mind Flay constexpr std::array mind_flay_ranks { - // spell - spell_rank_t{ 15407 }, - spell_rank_t{ 17311 }, - spell_rank_t{ 17312 }, - spell_rank_t{ 17313 }, - spell_rank_t{ 17314 }, - spell_rank_t{ 18807 }, + spell_container_t().set_spell( 15407 ), + spell_container_t().set_spell( 17311 ), + spell_container_t().set_spell( 17312 ), + spell_container_t().set_spell( 17313 ), + spell_container_t().set_spell( 17314 ), + spell_container_t().set_spell( 18807 ), }; struct mind_flay_t : public priest_spell_t { mind_flay_t( priest_t* player, util::string_view options_str ) : priest_spell_t( "mind_flay", player, player->talent.mind_flay.ok() - ? player->find_spell( player->find_rank( mind_flay_ranks ).spell_id ) + ? player->find_rank( mind_flay_ranks )->spell( player ) : spell_data_t::not_found() ) { parse_options( options_str ); @@ -502,16 +496,15 @@ struct mind_flay_t : public priest_spell_t // TODO: Backlash damage, effect 2 constexpr std::array shadow_word_death_ranks { - // spell - spell_rank_t{ 1309595 }, - spell_rank_t{ 1309633 }, - spell_rank_t{ 1309635 }, - spell_rank_t{ 1309636 }, + spell_container_t().set_spell( 1309595 ), + spell_container_t().set_spell( 1309633 ), + spell_container_t().set_spell( 1309635 ), + spell_container_t().set_spell( 1309636 ), }; struct shadow_word_death_t : public priest_spell_t { shadow_word_death_t( priest_t* player, util::string_view options_str ) : - priest_spell_t( "shadow_word_death", player, player->find_spell( player->find_rank( shadow_word_death_ranks ).spell_id ) ) + priest_spell_t( "shadow_word_death", player, player->find_rank( shadow_word_death_ranks )->spell( player ) ) { parse_options( options_str ); @@ -531,20 +524,19 @@ struct shadow_word_death_t : public priest_spell_t // Holy Fire constexpr std::array holy_fire_ranks { - // spell - spell_rank_t{ 14914 }, - spell_rank_t{ 15262 }, - spell_rank_t{ 15263 }, - spell_rank_t{ 15264 }, - spell_rank_t{ 15265 }, - spell_rank_t{ 15266 }, - spell_rank_t{ 15267 }, - spell_rank_t{ 15261 }, // Rank 8 is 15261 for some reason + spell_container_t().set_spell( 14914 ), + spell_container_t().set_spell( 15262 ), + spell_container_t().set_spell( 15263 ), + spell_container_t().set_spell( 15264 ), + spell_container_t().set_spell( 15265 ), + spell_container_t().set_spell( 15266 ), + spell_container_t().set_spell( 15267 ), + spell_container_t().set_spell( 15261 ), // Rank 8 is 15261 for some reason }; struct holy_fire_t : public priest_spell_t { holy_fire_t( priest_t* player, util::string_view options_str ) : - priest_spell_t( "holy_fire", player, player->find_spell( player->find_rank( holy_fire_ranks ).spell_id ) ) + priest_spell_t( "holy_fire", player, player->find_rank( holy_fire_ranks )->spell( player ) ) { parse_options( options_str ); } @@ -552,24 +544,18 @@ struct holy_fire_t : public priest_spell_t // Penance TODO: Check Penance vs Inner Focus - Right now only first bolt gets the crit bonus -struct penance_rank_t -{ - unsigned spell_id; // Penance - unsigned channel_id; // Penance (channel) - unsigned damage_id; // Penance (damage) -}; +// driver: channel, attack: damage bolt constexpr std::array penance_ranks { - // spell channel damage - penance_rank_t{ 402174, 402261, 402284 }, - penance_rank_t{ 1240720, 1240734, 1240727 }, - penance_rank_t{ 1240721, 1240736, 1240730 }, - penance_rank_t{ 1316995, 1316994, 1316993 }, + spell_container_t().set_spell( 402174 ).set_driver( 402261 ).set_attack( 402284 ), + spell_container_t().set_spell( 1240720 ).set_driver( 1240734 ).set_attack( 1240727 ), + spell_container_t().set_spell( 1240721 ).set_driver( 1240736 ).set_attack( 1240730 ), + spell_container_t().set_spell( 1316995 ).set_driver( 1316994 ).set_attack( 1316993 ), }; struct penance_damage_t : public priest_spell_t { penance_damage_t( priest_t* player ) : - priest_spell_t( "penance_damage", player, player->find_spell( player->find_rank( penance_ranks ).damage_id ) ) + priest_spell_t( "penance_damage", player, player->find_rank( penance_ranks )->attack( player ) ) { background = true; parse_power_in_light(); @@ -580,14 +566,14 @@ struct penance_t : public priest_spell_t { penance_t( priest_t* player, util::string_view options_str ) : priest_spell_t( "penance", player, player->talent.penance.ok() - ? player->find_spell( player->find_rank( penance_ranks ).spell_id ) + ? player->find_rank( penance_ranks )->spell( player ) : spell_data_t::not_found() ) { parse_options( options_str ); channeled = true; may_miss = may_crit = false; - parse_effect_data( player->find_spell( player->find_rank( penance_ranks ).channel_id )->effectN( 2 ) ); + parse_effect_data( player->find_rank( penance_ranks )->driver( player )->effectN( 2 ) ); tick_action = new penance_damage_t( player ); } }; @@ -596,19 +582,18 @@ struct penance_t : public priest_spell_t // TODO: Party heal constexpr std::array holy_nova_ranks { - // spell - spell_rank_t{ 15237 }, - spell_rank_t{ 15430 }, - spell_rank_t{ 15431 }, - spell_rank_t{ 27799 }, - spell_rank_t{ 27800 }, - spell_rank_t{ 27801 }, + spell_container_t().set_spell( 15237 ), + spell_container_t().set_spell( 15430 ), + spell_container_t().set_spell( 15431 ), + spell_container_t().set_spell( 27799 ), + spell_container_t().set_spell( 27800 ), + spell_container_t().set_spell( 27801 ), }; struct holy_nova_t : public priest_spell_t { holy_nova_t( priest_t* player, util::string_view options_str ) : priest_spell_t( "holy_nova", player, player->talent.holy_nova.ok() - ? player->find_spell( player->find_rank( holy_nova_ranks ).spell_id ) + ? player->find_rank( holy_nova_ranks )->spell( player ) : spell_data_t::not_found() ) { parse_options( options_str ); @@ -619,14 +604,13 @@ struct holy_nova_t : public priest_spell_t // Starshards constexpr std::array starshards_ranks { - // spell - spell_rank_t{ 10797 }, - spell_rank_t{ 19296 }, - spell_rank_t{ 19299 }, - spell_rank_t{ 19302 }, - spell_rank_t{ 19303 }, - spell_rank_t{ 19304 }, - spell_rank_t{ 19305 }, + spell_container_t().set_spell( 10797 ), + spell_container_t().set_spell( 19296 ), + spell_container_t().set_spell( 19299 ), + spell_container_t().set_spell( 19302 ), + spell_container_t().set_spell( 19303 ), + spell_container_t().set_spell( 19304 ), + spell_container_t().set_spell( 19305 ), }; struct starshards_t : public priest_spell_t { @@ -642,12 +626,11 @@ struct starshards_t : public priest_spell_t // TODO: Health cost, effect 1 constexpr std::array dark_sacrifice_ranks { - // spell - spell_rank_t{ 1277324 }, - spell_rank_t{ 1277325 }, - spell_rank_t{ 1277326 }, - spell_rank_t{ 1277327 }, - spell_rank_t{ 1277328 }, + spell_container_t().set_spell( 1277324 ), + spell_container_t().set_spell( 1277325 ), + spell_container_t().set_spell( 1277326 ), + spell_container_t().set_spell( 1277327 ), + spell_container_t().set_spell( 1277328 ), }; struct dark_sacrifice_t : public priest_spell_t { diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 2836c0138a..14bceddbb5 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -99,23 +99,6 @@ struct player_report_extension_t virtual void html_customsection(report::sc_html_stream&) = 0; }; -/* Spell rank tables - * - * Spell rank tables group related spells together by rank to make it easier to connect auras/drivers/etc related - * to a primary spell. The basic version just has a single spell_id but it's easy to make custom versions with - * related spells. - */ -struct spell_rank_t -{ - unsigned spell_id; -}; - -enum spell_rank_e : int -{ - RANK_MAX_USABLE = 0, // highest rank usable at the player level - RANK_MAX = -1, // highest rank in the table -}; - struct player_t : public actor_t { static const int default_level = MAX_LEVEL; From 0503e9ab1e6af7edb82bbf1c380545bfa6c97f69 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Sun, 25 Oct 2026 18:53:42 -0300 Subject: [PATCH 41/42] [Priest] Vampiric Embrace, Hex of Weakness, and Touch of Weakness --- engine/class_modules/sc_priest.cpp | 184 +++++++++++++++++++++++++++-- 1 file changed, 177 insertions(+), 7 deletions(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index 7785aef116..c22feb41c9 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -23,7 +23,10 @@ struct priest_td_t final : public actor_target_data_t struct debuffs_t { + buff_t* hex_of_weakness = nullptr; buff_t* shadow_weaving = nullptr; + buff_t* touch_of_weakness = nullptr; + buff_t* vampiric_embrace = nullptr; } debuffs; priest_td_t( player_t* target, priest_t& p ); @@ -49,6 +52,7 @@ struct priest_t final : public parse_player_effects_t void parse_action_effects( action_t* ); void init_special_effects() override; + void create_actions() override; void activate() override; double composite_attribute_multiplier( attribute_e ) const override; @@ -56,10 +60,14 @@ struct priest_t final : public parse_player_effects_t priest_td_t* get_target_data( player_t* target ) const override; // Priest racials are not in the racial spell table, so check the spell's race mask instead - const spell_data_t* find_racial_rank( util::span ranks ) const + // Rank 1 is checked first so other races and levels below rank 1 never reach find_rank + const spell_data_t* find_racial_rank( util::span ranks, + rank_ptype_t field = &spell_container_t::spell ) const { - auto spell = find_rank( ranks )->spell( this ); - return spell->race_mask() & util::race_mask( race ) ? spell : spell_data_t::not_found(); + if ( !( ranks.front().spell( this )->race_mask() & util::race_mask( race ) ) ) + return spell_data_t::not_found(); + + return std::invoke( field, find_rank( ranks ), this ); } std::string default_potion() const override { return priest_apl::potion( this ); } @@ -75,6 +83,7 @@ struct priest_t final : public parse_player_effects_t buff_t* holy_purpose = nullptr; buff_t* inner_focus = nullptr; buff_t* shadowform = nullptr; + buff_t* touch_of_weakness = nullptr; } buff; struct talents_t @@ -91,16 +100,25 @@ struct priest_t final : public parse_player_effects_t player_talent_t searing_light; player_talent_t shadow_weaving; player_talent_t shadowform; + player_talent_t vampiric_embrace; player_talent_t wand_specialization; } talent; struct spells_t { const spell_data_t* dark_inspiration = spell_data_t::nil(); + const spell_data_t* hex_of_weakness = spell_data_t::nil(); const spell_data_t* holy_purpose = spell_data_t::nil(); const spell_data_t* shadow_weaving = spell_data_t::nil(); + const spell_data_t* touch_of_weakness = spell_data_t::nil(); + const spell_data_t* touch_of_weakness_debuff = spell_data_t::nil(); } spell; + struct actives_t + { + action_t* touch_of_weakness = nullptr; + } active; + private: target_specific_t _target_data; }; @@ -111,7 +129,10 @@ priest_td_t::priest_td_t( player_t* target, priest_t& p ) : actor_target_data_t( dots.holy_fire = target->get_dot( "holy_fire", &p ); dots.shadow_word_pain = target->get_dot( "shadow_word_pain", &p ); - debuffs.shadow_weaving = make_buff( *this, "shadow_weaving", p.spell.shadow_weaving ); + debuffs.hex_of_weakness = make_buff( *this, "hex_of_weakness", p.spell.hex_of_weakness ); + debuffs.shadow_weaving = make_buff( *this, "shadow_weaving", p.spell.shadow_weaving ); + debuffs.touch_of_weakness = make_buff( *this, "touch_of_weakness", p.spell.touch_of_weakness_debuff ); + debuffs.vampiric_embrace = make_buff( *this, "vampiric_embrace", p.talent.vampiric_embrace ); } struct priest_module_t final : public module_t @@ -175,6 +196,29 @@ struct priest_action_t : public parse_action_effects_t using priest_spell_t = priest_action_t; +// Hex of Weakness + +constexpr std::array hex_of_weakness_ranks { + spell_container_t().set_spell( 9035 ), + spell_container_t().set_spell( 19281 ), + spell_container_t().set_spell( 19282 ), + spell_container_t().set_spell( 19283 ), + spell_container_t().set_spell( 19284 ), + spell_container_t().set_spell( 19285 ), +}; + +// Touch of Weakness + +// spell: self buff, debuff: damage and attack power debuff on the attacker +constexpr std::array touch_of_weakness_ranks { + spell_container_t().set_spell( 2652 ).set_debuff( 2943 ), + spell_container_t().set_spell( 19261 ).set_debuff( 19249 ), + spell_container_t().set_spell( 19262 ).set_debuff( 19251 ), + spell_container_t().set_spell( 19264 ).set_debuff( 19252 ), + spell_container_t().set_spell( 19265 ).set_debuff( 19253 ), + spell_container_t().set_spell( 19266 ).set_debuff( 19254 ), +}; + void priest_t::init_base_stats() { parse_player_effects_t::init_base_stats(); @@ -205,15 +249,19 @@ void priest_t::init_spells() talent.shadow_weaving = find_talent_spell( talent_tree::CLASS, "Shadow Weaving" ); talent.searing_light = find_talent_spell( talent_tree::CLASS, "Searing Light" ); talent.shadowform = find_talent_spell( talent_tree::CLASS, "Shadowform" ); + talent.vampiric_embrace = find_talent_spell( talent_tree::CLASS, "Vampiric Embrace" ); // Wand only, all schools because wand damage can be any school // TODO: Apply to the wand auto attack once implemented talent.wand_specialization = find_talent_spell( talent_tree::CLASS, "Wand Specialization" ); deregister_passive_spell( talent.wand_specialization ); - spell.dark_inspiration = find_spell( 1326168 ); - spell.holy_purpose = talent.searing_light->effectN( 2 ).trigger(); - spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); + spell.dark_inspiration = find_spell( 1326168 ); + spell.hex_of_weakness = find_racial_rank( hex_of_weakness_ranks ); + spell.holy_purpose = talent.searing_light->effectN( 2 ).trigger(); + spell.shadow_weaving = talent.shadow_weaving->effectN( 1 ).trigger(); + spell.touch_of_weakness = find_racial_rank( touch_of_weakness_ranks ); + spell.touch_of_weakness_debuff = find_racial_rank( touch_of_weakness_ranks, &spell_container_t::debuff ); } void priest_t::create_buffs() @@ -236,6 +284,8 @@ void priest_t::create_buffs() buff.shadowform = make_buff( this, "shadowform", talent.shadowform ); + buff.touch_of_weakness = make_buff( this, "touch_of_weakness", spell.touch_of_weakness ); + parse_effects( buff.shadowform ); parse_target_effects( d_fn( &priest_td_t::debuffs_t::shadow_weaving ), spell.shadow_weaving ); } @@ -295,6 +345,26 @@ void priest_t::init_special_effects() new dbc_proc_callback_t( this, *driver ); } + if ( spell.touch_of_weakness->ok() ) + { + const auto driver = new special_effect_t( this ); + driver->name_str = spell.touch_of_weakness->name_cstr(); + driver->spell_id = spell.touch_of_weakness->id(); + special_effects.push_back( driver ); + + // Proc events are delayed, so simultaneous melee hits can queue more than one before the buff expires + callbacks.register_callback_execute_function( driver->spell_id, [ this ]( auto, auto, auto, action_state_t* s ) { + if ( !buff.touch_of_weakness->check() ) + return; + + active.touch_of_weakness->execute_on_target( s->action->player ); + buff.touch_of_weakness->expire(); + } ); + + auto cb = new dbc_proc_callback_t( this, *driver ); + cb->activate_with_buff( buff.touch_of_weakness ); + } + parse_player_effects_t::init_special_effects(); } @@ -650,6 +720,92 @@ struct dark_sacrifice_t : public priest_spell_t } }; +// Vampiric Embrace + +// TODO: Party heal +struct vampiric_embrace_t : public priest_spell_t +{ + vampiric_embrace_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "vampiric_embrace", player, player->talent.vampiric_embrace.ok() + ? player->talent.vampiric_embrace.spell() + : spell_data_t::not_found() ) + { + parse_options( options_str ); + } + + void impact( action_state_t* s ) override + { + priest_spell_t::impact( s ); + + if ( result_is_hit( s->result ) ) + p()->get_target_data( s->target )->debuffs.vampiric_embrace->trigger(); + } +}; + +// Hex of Weakness + +struct hex_of_weakness_t : public priest_spell_t +{ + hex_of_weakness_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "hex_of_weakness", player, player->spell.hex_of_weakness ) + { + parse_options( options_str ); + } + + void impact( action_state_t* s ) override + { + priest_spell_t::impact( s ); + + if ( result_is_hit( s->result ) ) + p()->get_target_data( s->target )->debuffs.hex_of_weakness->trigger(); + } +}; + +// Touch of Weakness + +struct touch_of_weakness_damage_t : public priest_spell_t +{ + touch_of_weakness_damage_t( priest_t* player ) : + priest_spell_t( "touch_of_weakness_damage", player, player->spell.touch_of_weakness_debuff ) + { + background = true; + } + + void impact( action_state_t* s ) override + { + priest_spell_t::impact( s ); + + if ( result_is_hit( s->result ) ) + p()->get_target_data( s->target )->debuffs.touch_of_weakness->trigger(); + } +}; + +struct touch_of_weakness_t : public priest_spell_t +{ + touch_of_weakness_t( priest_t* player, util::string_view options_str ) : + priest_spell_t( "touch_of_weakness", player, player->spell.touch_of_weakness ) + { + parse_options( options_str ); + harmful = false; + target = player; + } + + void execute() override + { + priest_spell_t::execute(); + + p()->buff.touch_of_weakness->trigger(); + } + + bool ready() override + { + if ( p()->buff.touch_of_weakness->check() ) + return false; + + return priest_spell_t::ready(); + } +}; + // Inner Focus struct inner_focus_t : public priest_spell_t @@ -735,6 +891,12 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return new starshards_t( this, options_str ); if ( name == "dark_sacrifice" ) return new dark_sacrifice_t( this, options_str ); + if ( name == "vampiric_embrace" ) + return new vampiric_embrace_t( this, options_str ); + if ( name == "hex_of_weakness" ) + return new hex_of_weakness_t( this, options_str ); + if ( name == "touch_of_weakness" ) + return new touch_of_weakness_t( this, options_str ); if ( name == "inner_focus" ) return new inner_focus_t( this, options_str ); if ( name == "shadowform" ) @@ -743,6 +905,14 @@ action_t* priest_t::create_action( util::string_view name, util::string_view opt return parse_player_effects_t::create_action( name, options_str ); } +void priest_t::create_actions() +{ + if ( spell.touch_of_weakness->ok() ) + active.touch_of_weakness = new touch_of_weakness_damage_t( this ); + + parse_player_effects_t::create_actions(); +} + void priest_t::init_action_list() { if ( !action_list_str.empty() ) From 7711489eee726307642bd9552b0fa75373620db0 Mon Sep 17 00:00:00 2001 From: Lucas Kern Date: Sun, 25 Oct 2026 19:01:30 -0300 Subject: [PATCH 42/42] [Priest] Dark Sacrifice spirit regen spread evenly over ticks TODO: verify this behavior --- engine/class_modules/sc_priest.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/engine/class_modules/sc_priest.cpp b/engine/class_modules/sc_priest.cpp index c22feb41c9..22321d9e9c 100644 --- a/engine/class_modules/sc_priest.cpp +++ b/engine/class_modules/sc_priest.cpp @@ -704,6 +704,9 @@ constexpr std::array dark_sacrifice_ranks { }; struct dark_sacrifice_t : public priest_spell_t { + // TODO: Check if spirit is snapshot on cast or updates dynamically + double spirit_per_tick = 0.0; + dark_sacrifice_t( priest_t* player, util::string_view options_str ) : priest_spell_t( "dark_sacrifice", player, player->find_racial_rank( dark_sacrifice_ranks ) ) { @@ -714,9 +717,14 @@ struct dark_sacrifice_t : public priest_spell_t void execute() override { + spirit_per_tick = player->cache.spirit() / ( dot_duration / base_tick_time ); + priest_spell_t::execute(); + } - player->resource_gain( RESOURCE_MANA, player->cache.spirit(), gain, this ); + double composite_energize_amount( const action_state_t* s ) const override + { + return priest_spell_t::composite_energize_amount( s ) + spirit_per_tick; } };