Skip to main content

wowlab_engine_combat/systems/buffs/
modifiers.rs

1use super::*;
2
3fn spell_can_be_reflected(state: &CombatState, spell: wowlab_types::sim::SpellIdx) -> bool {
4    let attributes = state
5        .config
6        .game_data
7        .spell_attributes(spell)
8        .unwrap_or_default();
9    let has = |kind| wowlab_engine_domain::dbc::spell_attribute_is(attributes, kind);
10    let explicitly_allowed =
11        has(wowlab_engine_domain::dbc::SpellAttributeKind::AllowSpellReflection);
12    let eligible_class = (state
13        .config
14        .game_data
15        .defense_type(spell)
16        .and_then(|raw| wowlab_engine_domain::dbc::DefenseType::try_from(raw).ok())
17        == Some(wowlab_engine_domain::dbc::DefenseType::Magic)
18        && !has(wowlab_engine_domain::dbc::SpellAttributeKind::Ability))
19        || explicitly_allowed;
20
21    eligible_class
22        && !has(wowlab_engine_domain::dbc::SpellAttributeKind::NoReflection)
23        && !has(wowlab_engine_domain::dbc::SpellAttributeKind::NoImmunities)
24        && !has(wowlab_engine_domain::dbc::SpellAttributeKind::Passive)
25}
26
27pub(crate) fn resolve_incoming_spell_defense(
28    state: &CombatState,
29    buf: &DenseBuffer,
30    target: EnemyIdx,
31    profile_spell_id: u32,
32    school: wowlab_types::combat::DamageSchool,
33    rng: &mut dyn FnMut() -> f64,
34) -> IncomingSpellDefense {
35    let spell = wowlab_types::sim::SpellIdx::from_raw(profile_spell_id);
36
37    if !spell_can_be_reflected(state, spell) {
38        return IncomingSpellDefense::None;
39    }
40
41    let school_mask = wowlab_engine_domain::dbc::SpellSchoolMask::from(school);
42    let mut reflected = false;
43    let mut deflect_chance = 0.0;
44
45    for_each_active_target_aura_effect(state, buf, target, |stacks, effect| match effect {
46        BuffEffect::SpellReflection(schools) => {
47            reflected |= schools.intersects(school_mask);
48        }
49        BuffEffect::SpellDeflectChance(percent) => {
50            deflect_chance += percent * stacks;
51        }
52        _ => {}
53    });
54
55    if reflected {
56        IncomingSpellDefense::Reflected
57    } else if wowlab_engine_rng::proc_chance(rng, deflect_chance / HUNDRED) {
58        IncomingSpellDefense::Deflected
59    } else {
60        IncomingSpellDefense::None
61    }
62}
63
64/// `TrinityCore` selects the first active aura-361 effect for base-attack replacement.
65pub(crate) fn active_auto_attack_replacement(
66    state: &CombatState,
67    buf: &DenseBuffer,
68) -> Option<u32> {
69    let mut replacement = None;
70
71    for_each_active_aura_effect(state, buf, |_stacks, effect| {
72        if replacement.is_none() {
73            if let BuffEffect::AutoAttackReplacement(spell_id) = effect {
74                replacement = Some(*spell_id);
75            }
76        }
77    });
78
79    replacement
80}
81
82/// Resolve the first active aura-220 school override affecting either damage identity.
83pub(crate) fn active_spell_school_override(
84    view: &CombatView<'_>,
85    profile_spell_id: u32,
86    effect_spell_id: u32,
87) -> Option<wowlab_types::combat::DamageSchool> {
88    let mut school = None;
89
90    for_each_active_aura_effect(view.state, view.buf, |_stacks, effect| {
91        let BuffEffect::SpellSchoolFromEffect {
92            school: candidate,
93            source_spell_id,
94            effect_index,
95        } = effect
96        else {
97            return;
98        };
99
100        if school.is_none()
101            && [profile_spell_id, effect_spell_id]
102                .into_iter()
103                .any(|spell_id| {
104                    view.state.config.game_data.effect_affects_spell(
105                        wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
106                        *effect_index,
107                        wowlab_types::sim::SpellIdx::from_raw(spell_id),
108                    )
109                })
110        {
111            school = Some(*candidate);
112        }
113    });
114
115    school
116}
117
118/// Whether an active aura-275 effect grants this spell a stance-mask exception.
119pub(crate) fn active_stance_mask_exception(view: &CombatView<'_>, spell_id: u32) -> bool {
120    let mut allowed = false;
121
122    for_each_active_aura_effect(view.state, view.buf, |_stacks, effect| {
123        let BuffEffect::SpellStanceMaskFromEffect {
124            source_spell_id,
125            effect_index,
126        } = effect
127        else {
128            return;
129        };
130
131        allowed |= view.state.config.game_data.effect_affects_spell(
132            wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
133            *effect_index,
134            wowlab_types::sim::SpellIdx::from_raw(spell_id),
135        );
136    });
137
138    allowed
139}
140
141/// The unhasted swing period the player's active shapeshift form imposes, from `SpellShapeshiftForm.CombatRoundTime`.
142pub(crate) fn active_form_combat_round_time_ms(
143    state: &CombatState,
144    buf: &DenseBuffer,
145) -> Option<u32> {
146    let form = buf.player().shapeshift_form;
147
148    if form == 0 {
149        return None;
150    }
151
152    let active_form = state
153        .defs
154        .auras
155        .iter()
156        .find(|aura| aura.shapeshift_form == form)?;
157
158    (active_form.shapeshift_combat_round_time_ms > 0)
159        .then_some(active_form.shapeshift_combat_round_time_ms)
160}
161
162pub(crate) fn target_aura_mechanic_mask(
163    state: &CombatState,
164    buf: &DenseBuffer,
165    target: EnemyIdx,
166    now: SimTime,
167) -> wowlab_engine_domain::dbc::MechanicMask {
168    use wowlab_engine_domain::dbc::ResolvedGameDataSemanticExt as _;
169
170    let mut mask = wowlab_engine_domain::dbc::MechanicMask::empty();
171
172    for key in buf.aura_keys() {
173        if key.affected() != ActorId::Enemy(target)
174            || buf
175                .aura(key)
176                .is_none_or(|slot| !slot.is_active(now.as_secs_f64()))
177        {
178            continue;
179        }
180
181        let Some(local) = state.aura_local_for_key(key) else {
182            continue;
183        };
184        let aura_id = state.aura(local).aura_id;
185
186        mask |= state
187            .config
188            .game_data
189            .all_effect_mechanic_mask(wowlab_types::sim::SpellIdx::from_raw(aura_id));
190    }
191
192    mask
193}
194
195/// Resolves the player that owns spell modifiers for a casting actor.
196/// Trinity resolves player casts to that player and pet or totem casts to the owning player.
197/// Unowned external and enemy actors have no spell-mod owner.
198#[must_use]
199pub(crate) const fn spell_modifier_owner(source: ActorId) -> Option<ActorId> {
200    match source {
201        ActorId::Player | ActorId::Pet(_) => Some(ActorId::Player),
202        ActorId::External | ActorId::Enemy(_) => None,
203    }
204}
205
206/// Resolve an active affected-spell modifier with Trinity's flat-before-percent ordering.
207pub(crate) fn active_spell_modifier_value(
208    view: ActorView<'_>,
209    spell: wowlab_types::sim::SpellIdx,
210    property: wowlab_engine_domain::dbc::ModifierPropertyKind,
211    initial: f64,
212) -> f64 {
213    let ActorView {
214        state,
215        buf,
216        actor: source,
217    } = view;
218    let initial = match property {
219        wowlab_engine_domain::dbc::ModifierPropertyKind::HitChance => {
220            state.config.game_data.hit_chance_modifier(spell, initial)
221        }
222        wowlab_engine_domain::dbc::ModifierPropertyKind::TargetResistance => state
223            .config
224            .game_data
225            .target_resistance_modifier(spell, initial),
226        wowlab_engine_domain::dbc::ModifierPropertyKind::DispelResistance => state
227            .config
228            .game_data
229            .dispel_resistance_modifier(spell, initial),
230        _ => initial,
231    };
232    let mut flat = 0.0;
233    let mut percent = 1.0;
234
235    if let Some(owner) = spell_modifier_owner(source) {
236        for_each_active_actor_aura_effect(ActorView::new(state, buf, owner), |stacks, effect| {
237            let BuffEffect::SpellModifierFromEffect {
238                value,
239                source_spell_id,
240                effect_index,
241                property: effect_property,
242                operation,
243            } = effect
244            else {
245                return;
246            };
247
248            if *effect_property != property
249                || !state.config.game_data.effect_affects_spell(
250                    wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
251                    *effect_index,
252                    spell,
253                )
254            {
255                return;
256            }
257
258            let points = dbc_effect_points(state, *source_spell_id, *effect_index, stacks);
259
260            match operation {
261                ModifierOperation::Flat => flat += value * points,
262                ModifierOperation::Percent => percent *= 1.0 + value * points / HUNDRED,
263                _ => {}
264            }
265        });
266    }
267
268    (initial + flat) * percent
269}
270
271/// Resolve active all-points and exact-effect modifiers in server order.
272pub(crate) fn active_effect_points(
273    view: ActorView<'_>,
274    spell: wowlab_types::sim::SpellIdx,
275    effect_index: u8,
276    initial: f64,
277) -> f64 {
278    let points = active_spell_modifier_value(
279        view,
280        spell,
281        wowlab_engine_domain::dbc::ModifierPropertyKind::Points,
282        initial,
283    );
284
285    wowlab_engine_domain::dbc::modifier_property_for_effect(effect_index)
286        .map_or(points, |property| {
287            active_spell_modifier_value(view, spell, property, points)
288        })
289}
290
291pub(crate) fn active_effect_amplitude(
292    view: ActorView<'_>,
293    spell: wowlab_types::sim::SpellIdx,
294    initial: f64,
295) -> f64 {
296    active_spell_modifier_value(
297        view,
298        spell,
299        wowlab_engine_domain::dbc::ModifierPropertyKind::Amplitude,
300        initial,
301    )
302}
303
304/// Resolve an active DBC spell modifier with Trinity's flat-before-percent ordering.
305pub(crate) fn active_targeting_value(
306    view: ActorView<'_>,
307    spell: wowlab_types::sim::SpellIdx,
308    property: wowlab_engine_domain::dbc::ModifierPropertyKind,
309    initial: f64,
310) -> f64 {
311    let ActorView {
312        state,
313        buf,
314        actor: source,
315    } = view;
316    let mut flat = 0.0;
317    let mut percent = 1.0;
318
319    let Some(owner) = spell_modifier_owner(source) else {
320        return initial;
321    };
322
323    for_each_active_actor_aura_effect(ActorView::new(state, buf, owner), |stacks, effect| {
324        let BuffEffect::SpellTargetingFromEffect {
325            value,
326            source_spell_id,
327            effect_index,
328            property: effect_property,
329            operation,
330        } = effect
331        else {
332            return;
333        };
334
335        if *effect_property != property
336            || !state.config.game_data.effect_affects_spell(
337                wowlab_types::sim::SpellIdx::from_raw(*source_spell_id),
338                *effect_index,
339                spell,
340            )
341        {
342            return;
343        }
344
345        let count = dbc_effect_points(state, *source_spell_id, *effect_index, stacks);
346
347        match operation {
348            ModifierOperation::Flat => flat += value * count,
349            ModifierOperation::Percent => percent *= (1.0 + value / HUNDRED).powf(count),
350            _ => {}
351        }
352    });
353
354    ((initial + flat) * percent).max(0.0)
355}