wowlab_engine_combat/systems/
mastery.rs1use wowlab_engine_domain::dbc::SpellSchoolMask;
4use wowlab_types::{
5 combat::DamageSchool,
6 constants::HUNDRED,
7 sim::{ActorId, EnemyIdx, SpellIdx},
8};
9
10use crate::state::{CombatInstance, CombatProgram};
11
12#[derive(Debug)]
13pub struct MasteryCtx<'a> {
14 pub program: &'a CombatProgram,
15 pub instance: &'a CombatInstance,
16 pub spell_id: u32,
17 pub(crate) spell_action: crate::state::program::LocalDamageActionIdx,
18 pub school: DamageSchool,
19 pub mastery: f64,
20 pub player_crit_pct: f64,
21 pub mastery_spell: SpellIdx,
22 pub(crate) mastery_action: Option<crate::state::program::LocalDamageActionIdx>,
23 pub is_periodic: bool,
24 pub is_pet: bool,
25 pub source: ActorId,
26 pub target: Option<EnemyIdx>,
29 pub now: wowlab_types::sim::SimTime,
30}
31
32#[derive(Clone, Copy, Debug)]
33pub struct MasteryEvent {
34 pub action: crate::DamageActionId,
35 pub school: DamageSchool,
36 pub mastery: f64,
37 pub player_crit_pct: f64,
38 pub mastery_action: crate::DamageActionId,
39 pub is_periodic: bool,
40 pub is_pet: bool,
41 pub source: ActorId,
42 pub target: Option<EnemyIdx>,
43 pub now: wowlab_types::sim::SimTime,
44}
45
46impl MasteryCtx<'_> {
47 #[must_use]
49 pub fn new<'a>(
50 program: &'a CombatProgram,
51 instance: &'a CombatInstance,
52 event: MasteryEvent,
53 ) -> Option<MasteryCtx<'a>> {
54 Some(MasteryCtx {
55 program,
56 instance,
57 spell_id: event.action.spell_id(),
58 spell_action: program.damage.validated_action_id(event.action)?,
59 school: event.school,
60 mastery: event.mastery,
61 player_crit_pct: event.player_crit_pct,
62 mastery_spell: SpellIdx::from_raw(event.mastery_action.spell_id()),
63 mastery_action: Some(program.damage.validated_action_id(event.mastery_action)?),
64 is_periodic: event.is_periodic,
65 is_pet: event.is_pet,
66 source: event.source,
67 target: event.target,
68 now: event.now,
69 })
70 }
71
72 #[must_use]
74 pub fn active_weapon_imbue_count(&self) -> usize {
75 self.program
76 .weapon_imbue_bindings
77 .iter()
78 .filter(|binding| self.instance.buffer.weapon_imbue_at(binding.slot).is_active != 0)
79 .count()
80 }
81
82 #[must_use]
84 pub fn is_aura_active(&self, local: crate::LocalAuraIdx) -> bool {
85 super::auras::is_aura_active(
86 &crate::context::CombatView::new(self.program, self.instance),
87 local,
88 self.source,
89 self.target,
90 )
91 }
92
93 #[must_use]
95 pub fn points(&self, effect: u8) -> f64 {
96 self.mastery_action
97 .and_then(|action| self.program.damage.action_effect(action, effect))
98 .map_or(0.0, |effect| {
99 self.program
100 .damage
101 .live_effect_points_local(effect, self.mastery)
102 })
103 }
104
105 #[must_use]
107 pub fn bonus(&self, effect: u8) -> f64 {
108 1.0 + self.points(effect) / HUNDRED
109 }
110
111 #[must_use]
113 pub fn spell_has_label(&self, label: i32) -> bool {
114 self.program
115 .damage
116 .action_at(self.spell_action)
117 .labels
118 .contains(&label)
119 }
120
121 #[must_use]
123 pub fn has_school(&self, school: DamageSchool) -> bool {
124 let expected = SpellSchoolMask::from(school);
125
126 self.program
127 .damage
128 .action_at(self.spell_action)
129 .school_mask
130 .contains(expected)
131 }
132
133 #[must_use]
135 pub fn affects_spell(&self, effect: u8) -> bool {
136 self.mastery_action
137 .and_then(|mastery| self.program.damage.action_effect(mastery, effect))
138 .is_some_and(|source| {
139 self.program
140 .damage
141 .effect_affects_action(source, self.spell_action)
142 })
143 }
144
145 #[must_use]
147 pub fn effect_affects_spell(&self, effect: crate::DamageEffectId) -> bool {
148 self.program
149 .damage
150 .effect_id_plan(effect)
151 .is_some_and(|(source, _)| {
152 self.program
153 .damage
154 .effect_affects_action(source, self.spell_action)
155 })
156 }
157
158 #[must_use]
160 pub fn affected_spell_bonus(&self, effect: u8) -> f64 {
161 if self.affects_spell(effect) {
162 self.bonus(effect)
163 } else {
164 1.0
165 }
166 }
167
168 #[must_use]
170 pub fn affected_bonus_any(&self, effects: &[u8]) -> f64 {
171 effects
172 .iter()
173 .copied()
174 .find(|&effect| self.affects_spell(effect))
175 .map_or(1.0, |effect| self.bonus(effect))
176 }
177
178 #[must_use]
180 pub fn direct_or_periodic_affected_bonus_any(
181 &self,
182 direct_effects: &[u8],
183 periodic_effects: &[u8],
184 ) -> f64 {
185 if self.is_periodic {
186 self.affected_bonus_any(periodic_effects)
187 } else {
188 self.affected_bonus_any(direct_effects)
189 }
190 }
191
192 #[must_use]
194 pub fn pet_bonus(&self, effect: u8) -> f64 {
195 if self.is_pet { self.bonus(effect) } else { 1.0 }
196 }
197
198 #[must_use]
200 pub fn target_health_pct(&self) -> f64 {
201 self.target
202 .and_then(|target| {
203 self.instance
204 .enemy_health_fraction(self.program, target, self.now)
205 })
206 .unwrap_or(1.0)
207 }
208
209 #[must_use]
211 pub fn active_guardian_count(&self) -> usize {
212 self.instance
213 .runtime
214 .companions
215 .guardians
216 .iter()
217 .flatten()
218 .count()
219 }
220}
221
222pub type MasteryFn = fn(&MasteryCtx<'_>) -> f64;
223
224#[must_use]
226pub const fn no_damage_bonus(_ctx: &MasteryCtx<'_>) -> f64 {
227 1.0
228}
229
230#[must_use]
232pub const fn no_crit_damage(_ctx: &MasteryCtx<'_>) -> f64 {
233 0.0
234}