wowlab_engine_combat/systems/
mastery.rs1use wowlab_engine_domain::{
4 dbc::{ResolvedGameDataSemanticExt as _, SpellSchoolMask},
5 rotation::DenseBuffer,
6};
7use wowlab_types::{
8 combat::DamageSchool,
9 constants::HUNDRED,
10 sim::{ActorId, EnemyIdx, SpellIdx},
11};
12
13use crate::state::CombatState;
14
15#[derive(Debug)]
16pub struct MasteryCtx<'a> {
17 pub state: &'a CombatState,
18 pub buf: &'a DenseBuffer,
19 pub spell_id: u32,
20 pub school: DamageSchool,
21 pub mastery: f64,
22 pub player_crit_pct: f64,
23 pub mastery_spell: SpellIdx,
24 pub is_periodic: bool,
25 pub is_pet: bool,
26 pub source: ActorId,
27 pub target: Option<EnemyIdx>,
30 pub now: wowlab_types::sim::SimTime,
31}
32
33impl MasteryCtx<'_> {
34 #[must_use]
36 pub fn active_weapon_imbue_count(&self) -> usize {
37 self.state
38 .defs
39 .weapon_imbue_bindings
40 .iter()
41 .filter(|binding| self.is_weapon_imbue_active(binding.key))
42 .count()
43 }
44
45 #[must_use]
47 pub fn is_aura_active(&self, local: crate::LocalAuraIdx) -> bool {
48 super::auras::is_aura_active(
49 &crate::context::CombatView::new(self.state, self.buf),
50 local,
51 self.source,
52 self.target,
53 )
54 }
55
56 #[must_use]
58 pub fn is_aura_active_id(&self, aura_id: u32) -> bool {
59 self.state
60 .aura_local(aura_id)
61 .is_some_and(|local| self.is_aura_active(local))
62 }
63
64 #[must_use]
66 pub fn points(&self, effect: u8) -> f64 {
67 self.state
68 .config
69 .game_data
70 .effect_points(self.mastery_spell, effect, self.mastery)
71 }
72
73 #[must_use]
75 pub fn bonus(&self, effect: u8) -> f64 {
76 1.0 + self.points(effect) / HUNDRED
77 }
78
79 #[must_use]
81 pub fn spell_has_label(&self, label: i32) -> bool {
82 self.state
83 .config
84 .game_data
85 .has_label(SpellIdx(self.spell_id), label)
86 }
87
88 #[must_use]
90 pub fn has_school(&self, school: DamageSchool) -> bool {
91 let expected = SpellSchoolMask::from(school);
92
93 self.state
94 .config
95 .game_data
96 .school_mask(SpellIdx(self.spell_id))
97 .is_some_and(|mask| SpellSchoolMask::from_dbc(mask).contains(expected))
98 }
99
100 #[must_use]
102 pub fn affects_spell(&self, effect: u8) -> bool {
103 self.state.config.game_data.effect_affects_spell(
104 self.mastery_spell,
105 effect,
106 SpellIdx(self.spell_id),
107 )
108 }
109
110 #[must_use]
112 pub fn affected_spell_bonus(&self, effect: u8) -> f64 {
113 if self.affects_spell(effect) {
114 self.bonus(effect)
115 } else {
116 1.0
117 }
118 }
119
120 #[must_use]
122 pub fn affected_bonus_any(&self, effects: &[u8]) -> f64 {
123 effects
124 .iter()
125 .copied()
126 .find(|&effect| self.affects_spell(effect))
127 .map_or(1.0, |effect| self.bonus(effect))
128 }
129
130 #[must_use]
132 pub fn direct_or_periodic_affected_bonus_any(
133 &self,
134 direct_effects: &[u8],
135 periodic_effects: &[u8],
136 ) -> f64 {
137 if self.is_periodic {
138 self.affected_bonus_any(periodic_effects)
139 } else {
140 self.affected_bonus_any(direct_effects)
141 }
142 }
143
144 #[must_use]
146 pub fn pet_bonus(&self, effect: u8) -> f64 {
147 if self.is_pet { self.bonus(effect) } else { 1.0 }
148 }
149
150 #[must_use]
152 pub fn target_health_pct(&self) -> f64 {
153 self.target
154 .and_then(|target| self.state.enemy_health_fraction(target, self.now))
155 .unwrap_or(1.0)
156 }
157
158 #[must_use]
160 pub fn active_guardian_count(&self) -> usize {
161 self.state
162 .runtime
163 .companions
164 .guardians
165 .iter()
166 .flatten()
167 .count()
168 }
169
170 #[must_use]
172 fn is_weapon_imbue_active(&self, key: &str) -> bool {
173 self.buf
174 .weapon_imbue(key)
175 .is_some_and(|imbue| imbue.is_active != 0)
176 }
177}
178
179pub type MasteryFn = fn(&MasteryCtx<'_>) -> f64;
180
181#[must_use]
183pub const fn no_damage_bonus(_ctx: &MasteryCtx<'_>) -> f64 {
184 1.0
185}
186
187#[must_use]
189pub const fn no_crit_damage(_ctx: &MasteryCtx<'_>) -> f64 {
190 0.0
191}