Skip to main content

wowlab_engine_combat/systems/
mastery.rs

1//! Per-spec mastery: each spec's [`MasteryFn`] returns a hit's mastery damage multiplier.
2
3use 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    /// Hostile target used by target-specific mastery clauses.
28    /// Reflected hits re-executed against their caster have no hostile target.
29    pub target: Option<EnemyIdx>,
30    pub now: wowlab_types::sim::SimTime,
31}
32
33impl MasteryCtx<'_> {
34    /// Number of registered temporary weapon imbues currently active.
35    #[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    /// Whether the exact source/target aura instance is active for this hit.
46    #[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    /// Whether a registered aura id is active for this hit's exact source/target.
57    #[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    /// Live effect points for the mastery spell's effect.
65    #[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    /// `1 + live_effect_points / 100`.
74    #[must_use]
75    pub fn bonus(&self, effect: u8) -> f64 {
76        1.0 + self.points(effect) / HUNDRED
77    }
78
79    /// Whether the hit's spell carries the given `SpellLabel` id.
80    #[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    /// Whether the hit's DBC school mask contains `school`.
89    #[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    /// Is the hit's spell inside the mastery effect's affect list? `effect` is 1-based.
101    #[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    /// [`Self::bonus`] only when the mastery effect's affect list includes this hit's spell.
111    #[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    /// [`Self::bonus`] for the first listed effect whose affect list includes this hit's spell.
121    #[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    /// Select the direct or periodic mastery effect list and apply the first matching bonus.
131    #[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    /// [`Self::bonus`] only when the hit is dealt by a pet, else `1.0`.
145    #[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    /// Exact target-health fraction (`1.0` = full) from canonical encounter state.
151    #[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    /// Number of currently active temporary guardians.
159    #[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    /// Whether a named temporary weapon imbue is active.
171    #[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/// Mastery that does not modify direct damage (proc- or pet-based); the mechanic lives elsewhere.
182#[must_use]
183pub const fn no_damage_bonus(_ctx: &MasteryCtx<'_>) -> f64 {
184    1.0
185}
186
187/// Neutral `mastery_crit_damage` hook (no crit-damage bonus).
188#[must_use]
189pub const fn no_crit_damage(_ctx: &MasteryCtx<'_>) -> f64 {
190    0.0
191}