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