Skip to main content

wowlab_engine_combat/systems/auras/
apply.rs

1use super::{
2    ActorId, AuraKey, Event, HookCtx, LocalAuraIdx, SimTime, SpellGroup, SpellGroupRule,
3    application_state, async_stacks, attempt_weapon_enchants, aura_keys_for_actor_application,
4    aura_keys_for_application, combine_rolling_multiplier, context_aura_key,
5    emit_aura_refresh_event, expire_aura_instance, expire_conflicting_auras,
6    initialize_or_revive_periodic, is_rolling_periodic, process_health_threshold_application,
7    procs, refresh_absorbs, refresh_pet_actions_after_aura_change, refresh_snapshot,
8    rolling_ticks_left, sync_exclusive_group_enabled, synchronize_aura_dependents,
9    update_aura_slot,
10};
11
12pub(super) fn schedule_aura_expiry(ctx: &mut HookCtx<'_>, key: AuraKey) {
13    let expires_at = ctx
14        .buf
15        .aura(key)
16        .map_or(ctx.now, |a| SimTime::from_secs_f64(a.expires_at));
17
18    ctx.state.schedule(Event::AuraExpire {
19        t: expires_at,
20        key,
21        target: ctx
22            .effect_target
23            .map_or(ctx.target, |affected| match affected {
24                ActorId::Enemy(target) => Some(target),
25                ActorId::Player | ActorId::Pet(_) | ActorId::External => None,
26            }),
27    });
28}
29
30pub(super) fn enqueue_aura_apply_effects(ctx: &mut HookCtx<'_>, aura: &crate::state::AuraData) {
31    if aura.apply_effects.len == 0 {
32        return;
33    }
34
35    ctx.state
36        .runtime
37        .deferred_work
38        .push_effect(crate::state::PendingEffectProgram {
39            effects: aura.apply_effects,
40            profile_spell_id: if aura.apply_profile_spell_id == 0 {
41                aura.aura_id
42            } else {
43                aura.apply_profile_spell_id
44            },
45            repeat: 1,
46            source: ctx.source,
47            target: ctx.target.or_else(|| ctx.state.current_target()),
48            effect_target: ctx.effect_target,
49            flags: ctx.source_damage_flags,
50            originating_proc_driver_spell_id: if ctx
51                .source_damage_flags
52                .contains(crate::DamageFlags::PROC)
53            {
54                ctx.driver_spell_id()
55            } else {
56                0
57            },
58        });
59}
60
61pub(super) fn reset_aura_proc_charges(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
62    let configured_procs = ctx
63        .state
64        .defs
65        .impact_effect_procs
66        .iter()
67        .copied()
68        .enumerate()
69        .filter(|(_, proc)| proc.charge_aura == Some(local) && !proc.uses_stacks_for_charges)
70        .map(|(index, proc)| {
71            (
72                index,
73                procs::effective_proc_charges(ctx.state, ctx.buf, ctx.source, proc),
74            )
75        });
76    let resets = configured_procs
77        .filter(|(_, charges)| *charges > 0)
78        .collect::<Vec<_>>();
79
80    for (index, charges) in resets {
81        if let Some(remaining) = ctx.state.runtime.procs.impact_effect_charges.get_mut(index) {
82            *remaining = charges;
83        }
84    }
85}
86
87pub(super) fn enqueue_aura_application_proc(
88    ctx: &mut HookCtx<'_>,
89    aura: &crate::state::AuraData,
90    key: AuraKey,
91) {
92    let kind = if ctx.source_damage_flags.contains(crate::DamageFlags::PROC) {
93        crate::state::ProcSourceKind::Proc
94    } else {
95        crate::state::ProcSourceKind::Action
96    };
97    let originating_proc_driver_spell_id = if kind == crate::state::ProcSourceKind::Proc {
98        ctx.driver_spell_id()
99    } else {
100        0
101    };
102
103    ctx.state
104        .runtime
105        .deferred_work
106        .push_aura_application(crate::state::AuraApplicationEvent {
107            aura_spell_id: aura.aura_id,
108            source: key.source(),
109            affected: key.affected(),
110            target: ctx.target,
111            // SimC's buff callback passes buff->proc_data, so suppression policy
112            // belongs to the applied aura rather than the action that invoked it.
113            provenance: crate::state::ProcProvenance::from_spell(
114                &ctx.state.config.game_data,
115                aura.aura_id,
116                kind,
117            ),
118            originating_proc_driver_spell_id,
119        });
120}
121
122pub(super) fn finish_aura_application(
123    ctx: &mut HookCtx<'_>,
124    request: application_state::AuraSlotRequest<'_>,
125    is_fresh: bool,
126) {
127    let application_state::AuraSlotRequest { aura, key, .. } = request;
128
129    if is_fresh {
130        process_health_threshold_application(ctx.state, key.affected(), &aura.effects, ctx.now);
131    }
132
133    enqueue_aura_application_proc(ctx, aura, key);
134}
135
136pub(super) fn apply_spell_group_rule(
137    ctx: &mut HookCtx<'_>,
138    local: LocalAuraIdx,
139    group: SpellGroup,
140) {
141    match group.rule {
142        SpellGroupRule::Exclusive => {
143            expire_conflicting_auras(ctx, local, group.group_idx);
144            sync_exclusive_group_enabled(ctx, local, group.group_idx);
145        }
146    }
147}
148
149/// Applies an aura by local index.
150pub fn apply_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
151    apply_aura_with_duration(ctx, local, None);
152}
153
154/// Applies a rolling periodic aura with a multiplier for this application's damage.
155pub(crate) fn apply_aura_with_rolling_multiplier(
156    ctx: &mut HookCtx<'_>,
157    local: LocalAuraIdx,
158    rolling_multiplier: f64,
159) {
160    apply_aura_with_options(ctx, local, None, rolling_multiplier);
161}
162
163/// Applies an aura with an optional duration override.
164pub fn apply_aura_with_duration(
165    ctx: &mut HookCtx<'_>,
166    local: LocalAuraIdx,
167    duration_ms: Option<u32>,
168) {
169    apply_aura_with_options(ctx, local, duration_ms, 1.0);
170}
171
172pub(super) fn apply_aura_with_options(
173    ctx: &mut HookCtx<'_>,
174    local: LocalAuraIdx,
175    duration_ms: Option<u32>,
176    rolling_multiplier: f64,
177) {
178    if !rolling_multiplier.is_finite() || rolling_multiplier <= 0.0 {
179        return;
180    }
181
182    // #t(rust_unchecked_indexing) local is validated at build time
183    let aura = ctx.state.defs.auras[local.as_usize()];
184
185    if aura_application_blocked(ctx, &aura) {
186        return;
187    }
188
189    apply_aura_unblocked(ctx, local, duration_ms, rolling_multiplier);
190
191    if aura.application_followup_aura_id != 0
192        && let Some(followup) = ctx.state.aura_local(aura.application_followup_aura_id)
193    {
194        apply_aura_unblocked(ctx, followup, None, 1.0);
195    }
196}
197
198pub(super) fn aura_application_blocked(ctx: &HookCtx<'_>, aura: &crate::state::AuraData) -> bool {
199    for aura_id in aura.application_excluded_auras {
200        if aura_id == 0 {
201            continue;
202        }
203
204        let Some(local) = ctx.state.aura_local(aura_id) else {
205            continue;
206        };
207        let Some(key) = context_aura_key(ctx, local) else {
208            continue;
209        };
210
211        if ctx
212            .buf
213            .aura(key)
214            .is_some_and(|slot| slot.is_active(ctx.now.as_secs_f64()))
215        {
216            return true;
217        }
218    }
219
220    false
221}
222
223pub(super) fn apply_aura_unblocked(
224    ctx: &mut HookCtx<'_>,
225    local: LocalAuraIdx,
226    duration_ms: Option<u32>,
227    rolling_multiplier: f64,
228) {
229    // #t(rust_unchecked_indexing) local is validated at build time
230    let aura = ctx.state.defs.auras[local.as_usize()];
231
232    if let Some(group) = aura.spell_group {
233        apply_spell_group_rule(ctx, local, group);
234    }
235
236    let unscaled_dur_ms = duration_ms.unwrap_or_else(|| {
237        crate::systems::buffs::effective_aura_duration_ms(
238            &ctx.view().combat(),
239            aura.aura_id,
240            aura.base_duration_ms,
241        )
242    });
243    let base_dur_ms = if aura.duration_hasted && unscaled_dur_ms > 0 {
244        if let Some(periodic) = aura.periodic {
245            let base_period_ms = crate::systems::buffs::effective_periodic_tick_ms(
246                &ctx.view().combat(),
247                aura.aura_id,
248                periodic.tick_interval_ms,
249            );
250            let period_ms = crate::systems::buffs::effective_periodic_interval_ms(
251                &ctx.view().combat(),
252                aura.aura_id,
253                periodic.tick_interval_ms,
254                periodic.hasted_ticks,
255            );
256
257            fixed_tick_periodic_duration_ms(unscaled_dur_ms, base_period_ms, period_ms)
258        } else {
259            let haste = crate::systems::buffs::current_haste_mult(&ctx.view().combat());
260
261            wowlab_types::numeric::f64_to_u32_saturating_round(f64::from(unscaled_dur_ms) / haste)
262                .max(1)
263        }
264    } else {
265        unscaled_dur_ms
266    };
267
268    let application_keys = ctx.effect_target.map_or_else(
269        || aura_keys_for_application(ctx.state, local, ctx.source, ctx.target),
270        |affected| aura_keys_for_actor_application(ctx.state, local, ctx.source, affected),
271    );
272
273    let mut applied = false;
274
275    for key in application_keys.into_iter().flatten() {
276        applied |= apply_aura_instance(ctx, local, &aura, key, base_dur_ms, rolling_multiplier);
277    }
278
279    if applied && ctx.source == ActorId::Player {
280        attempt_weapon_enchants(ctx);
281    }
282}
283
284pub(super) fn fixed_tick_periodic_duration_ms(
285    unscaled_duration_ms: u32,
286    base_period_ms: u32,
287    period_ms: u32,
288) -> u32 {
289    unscaled_duration_ms
290        .checked_div(base_period_ms)
291        .unwrap_or_default()
292        .max(1)
293        .saturating_mul(period_ms)
294}
295
296fn effective_aura_base_duration_ms(
297    ctx: &HookCtx<'_>,
298    aura_id: u32,
299    affected: ActorId,
300    base_duration_ms: u32,
301) -> (f64, u32) {
302    let multiplier =
303        crate::systems::buffs::aura_time_rate_multiplier(ctx.view().for_actor(affected), aura_id);
304    let duration_ms = if base_duration_ms == 0 {
305        0
306    } else {
307        wowlab_types::numeric::f64_to_u32_saturating_round(f64::from(base_duration_ms) * multiplier)
308            .max(1)
309    };
310
311    (multiplier, duration_ms)
312}
313
314// #t(fn: rust_cyclomatic_complexity) aura application is one ordered transition across refresh, stack, snapshot, and scheduling policies.
315pub(super) fn apply_aura_instance(
316    ctx: &mut HookCtx<'_>,
317    local: LocalAuraIdx,
318    aura: &crate::state::AuraData,
319    key: AuraKey,
320    base_dur_ms: u32,
321    rolling_multiplier: f64,
322) -> bool {
323    let (time_rate_multiplier, base_dur_ms) =
324        effective_aura_base_duration_ms(ctx, aura.aura_id, key.affected(), base_dur_ms);
325
326    ctx.buf.ensure_aura_slot(key);
327
328    if aura.refresh_behavior == wowlab_types::data::RefreshBehavior::None
329        && ctx
330            .buf
331            .aura(key)
332            .is_some_and(|slot| slot.is_active(ctx.now.as_secs_f64()))
333    {
334        return false;
335    }
336
337    let base_dur_ms = if let Some(control) = aura.crowd_control {
338        let duration = if base_dur_ms == 0 {
339            crate::state::CrowdControlDuration::Permanent
340        } else {
341            crate::state::CrowdControlDuration::Finite(SimTime::from_millis(base_dur_ms))
342        };
343
344        match ctx.state.apply_crowd_control_duration(
345            key.affected(),
346            control.kind,
347            control.diminishing,
348            duration,
349            ctx.now,
350        ) {
351            crate::state::CrowdControlApplication::Applied {
352                duration: crate::state::CrowdControlDuration::Finite(duration),
353                ..
354            } => duration.as_millis(),
355            crate::state::CrowdControlApplication::Applied {
356                duration: crate::state::CrowdControlDuration::Permanent,
357                ..
358            } => 0,
359            crate::state::CrowdControlApplication::Immune => return false,
360        }
361    } else {
362        base_dur_ms
363    };
364    let base_dur = SimTime::from_millis(base_dur_ms);
365    let is_permanent = base_dur_ms == 0;
366    let slot_request = application_state::AuraSlotRequest {
367        aura,
368        key,
369        base_dur_ms,
370        base_dur,
371        is_permanent,
372    };
373
374    if retrigger_unticked_reverse_aura(ctx, local, aura, key) {
375        enqueue_aura_application_proc(ctx, aura, key);
376
377        return true;
378    }
379
380    let previous_expiry = ctx
381        .buf
382        .aura(key)
383        .filter(|slot| slot.is_occupied())
384        .map(|slot| slot.expires_at);
385
386    if aura.async_stacks && !is_permanent {
387        let is_fresh = async_stacks::apply_async_stacks(ctx, local, key, base_dur_ms, aura.doses);
388
389        initialize_or_revive_periodic(ctx, slot_request, is_fresh, previous_expiry);
390        refresh_snapshot(ctx, slot_request);
391        refresh_absorbs(ctx, slot_request);
392
393        if aura.shapeshift_form > 0 {
394            ctx.buf.player_mut().shapeshift_form = aura.shapeshift_form;
395            tracing::trace!(
396                aura_id = aura.aura_id,
397                form = aura.shapeshift_form,
398                "applied shapeshift form"
399            );
400        }
401
402        synchronize_aura_dependents(ctx);
403        refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
404        enqueue_aura_apply_effects(ctx, aura);
405        reset_aura_proc_charges(ctx, local);
406        enqueue_aura_application_proc(ctx, aura, key);
407
408        return true;
409    }
410
411    let rolling_old_ticks_left = if is_rolling_periodic(aura) {
412        rolling_ticks_left(ctx, key)
413    } else {
414        0.0
415    };
416    let Some(is_fresh) = update_aura_slot(ctx, slot_request) else {
417        return false;
418    };
419
420    if let Some(slot) = ctx.buf.aura_mut(key) {
421        slot.time_rate_multiplier = time_rate_multiplier;
422    }
423
424    initialize_or_revive_periodic(
425        ctx,
426        slot_request,
427        is_fresh
428            || aura.refresh_behavior == wowlab_types::data::RefreshBehavior::Clip
429            || aura.tick_behavior == crate::state::AuraTickBehavior::Clip,
430        previous_expiry,
431    );
432    refresh_snapshot(ctx, slot_request);
433    refresh_absorbs(ctx, slot_request);
434    combine_rolling_multiplier(
435        ctx,
436        slot_request,
437        rolling_old_ticks_left,
438        rolling_multiplier,
439    );
440
441    if ctx.state.defs.stealth_aura_id == Some(aura.aura_id) {
442        ctx.buf.player_mut().stealthed = 1;
443    }
444
445    if aura.shapeshift_form > 0 {
446        ctx.buf.player_mut().shapeshift_form = aura.shapeshift_form;
447        tracing::trace!(
448            aura_id = aura.aura_id,
449            form = aura.shapeshift_form,
450            "applied shapeshift form"
451        );
452    }
453
454    synchronize_aura_dependents(ctx);
455    schedule_aura_expiry(ctx, key);
456    refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
457    enqueue_aura_apply_effects(ctx, aura);
458    reset_aura_proc_charges(ctx, local);
459
460    finish_aura_application(ctx, slot_request, is_fresh);
461
462    true
463}
464
465pub(super) fn retrigger_unticked_reverse_aura(
466    ctx: &mut HookCtx<'_>,
467    local: LocalAuraIdx,
468    aura: &crate::state::AuraData,
469    key: AuraKey,
470) -> bool {
471    if !aura.reverse || aura.tick_behavior != crate::state::AuraTickBehavior::None {
472        return false;
473    }
474
475    let Some(current_stacks) = ctx
476        .buf
477        .aura(key)
478        .filter(|slot| slot.is_active(ctx.now.as_secs_f64()))
479        .map(|slot| slot.stacks)
480    else {
481        return false;
482    };
483
484    let change = i32::from(aura.doses.max(1));
485
486    if current_stacks <= change {
487        expire_aura_instance(ctx, local, key);
488    } else {
489        let stacks = if let Some(slot) = ctx.buf.aura_mut(key) {
490            slot.stacks -= change;
491
492            wowlab_types::numeric::i32_to_u8_saturating(slot.stacks)
493        } else {
494            return true;
495        };
496
497        emit_aura_refresh_event(ctx.state, ctx.sink, key, aura.aura_id, stacks, ctx.now);
498        synchronize_aura_dependents(ctx);
499        refresh_pet_actions_after_aura_change(ctx.state, ctx.buf, ctx.now);
500    }
501
502    true
503}