Skip to main content

wowlab_engine_combat/systems/buffs/
modifiers.rs

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