Skip to main content

wowlab_engine_combat/systems/
auras.rs

1//! Aura application, expiry, stacking, snapshotting, and exclusive-group handling.
2
3use wowlab_engine_domain::{dbc::SpellAuraInterruptFlags, rotation::DenseBuffer};
4use wowlab_engine_ports::Event;
5use wowlab_types::{
6    constants::HUNDRED,
7    sim::{ActorId, AuraKey, EnemyIdx, SimTime, SpellIdx},
8};
9
10use super::{
11    EffectExecution, async_stacks, attempt_weapon_enchants, execute_effect_range_for_actor,
12    execute_proc_effect_range_for_actor, fire_aura_application_procs,
13    process_health_threshold_application, procs, refresh_pet_actions_after_aura_change,
14};
15use crate::{
16    context::HookCtx,
17    state::{ActiveAbsorb, BuffEffect, CombatState, LocalAuraIdx, SpellGroup, SpellGroupRule},
18};
19
20pub(crate) mod apply;
21mod breaks;
22mod copy;
23mod deferred;
24pub(crate) mod expire;
25pub(crate) mod stacks;
26
27use apply::enqueue_aura_application_proc;
28#[cfg(test)]
29use apply::fixed_tick_periodic_duration_ms;
30#[cfg(test)]
31use apply::schedule_aura_expiry;
32pub(crate) use apply::{apply_aura, apply_aura_with_duration, apply_aura_with_rolling_multiplier};
33pub(crate) use breaks::{
34    break_auras_on_actor, break_auras_without_telemetry, break_stealth_if_active,
35};
36pub(crate) use copy::aura_remaining_ms;
37#[cfg(test)]
38pub(crate) use copy::copy_target_aura;
39pub(crate) use deferred::drain_deferred_work;
40#[cfg(test)]
41pub(crate) use expire::expire_aura_by_id;
42pub(crate) use expire::{expire_aura, expire_auras_on_actor};
43use expire::{expire_aura_instance, refresh_absorbs};
44pub(crate) use stacks::{
45    add_aura_stack, aura_stacks, aura_stacks_by_id, bypass_aura_active, consume_aura,
46    consume_aura_stack, consume_aura_stack_by_id, consume_aura_stacks, extend_aura, is_aura_active,
47    reduce_aura, take_aura_stacks,
48};
49
50mod application_state;
51
52#[cfg(test)]
53pub(crate) use application_state::capture_snapshot_fixture;
54#[cfg(test)]
55use application_state::pandemic_carry;
56#[cfg(test)]
57use application_state::pandemic_carry_ms;
58pub(crate) use application_state::{
59    aura_key_for, aura_keys_for_actor_application, aura_keys_for_application, capture_snapshot_for,
60    emit_aura_apply_event, emit_aura_refresh_event, refresh_aura_projections,
61    refresh_aura_snapshot_multiplier,
62};
63use application_state::{
64    aura_query_key_for, combine_rolling_multiplier, context_aura_key, emit_aura_expire_event,
65    expire_conflicting_auras, initialize_or_revive_periodic, is_rolling_periodic, refresh_snapshot,
66    refreshed_stacks, restore_active_shapeshift_form, revive_tick_chain, rolling_ticks_left,
67    sync_exclusive_group_enabled, synchronize_aura_dependents, update_aura_slot,
68};
69
70const MAX_DEFERRED_WORK_ITEMS: usize = 1_024;
71const MAX_AURA_APPLICATION_ACTORS: usize = 2;
72const PERMANENT_AURA_EXPIRES_AT_S: f64 = 1.0e12;
73
74/// SimC-compatible policy for moving a periodic aura between targets.
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76#[cfg(test)]
77#[non_exhaustive]
78pub(crate) enum DotCopyBehavior {
79    /// Start a new tick chain with the source's remaining duration.
80    Start,
81    /// Clone the source state without carrying destination duration.
82    CloneNoRefresh,
83}
84
85pub(super) fn reproject_aura_time_rates(ctx: &mut HookCtx<'_>) {
86    let keys: Vec<_> = ctx.buf.aura_keys().collect();
87
88    for key in keys {
89        let Some(slot) = ctx
90            .buf
91            .aura(key)
92            .copied()
93            .filter(|slot| slot.is_active(ctx.now.as_secs_f64()))
94        else {
95            continue;
96        };
97        let Some(local) = ctx.state.aura_local_for_key(key) else {
98            continue;
99        };
100        // BOUNDS: aura_local_for_key resolves indices from this definition table.
101        let aura = ctx.state.defs.auras[local.as_usize()];
102        let desired = crate::systems::buffs::aura_time_rate_multiplier(
103            ctx.view().for_actor(key.affected()),
104            aura.aura_id,
105        );
106        let current = if slot.time_rate_multiplier > 0.0 {
107            slot.time_rate_multiplier
108        } else {
109            1.0
110        };
111        let ratio = desired / current;
112
113        if (ratio - 1.0).abs() <= f64::EPSILON {
114            continue;
115        }
116
117        let now_s = ctx.now.as_secs_f64();
118
119        if let Some(active) = ctx.buf.aura_mut(key) {
120            if active.expires_at < PERMANENT_AURA_EXPIRES_AT_S {
121                active.expires_at = now_s + (active.expires_at - now_s).max(0.0) * ratio;
122            }
123
124            if active.tick_interval > 0.0 {
125                active.next_tick = now_s + (active.next_tick - now_s).max(0.0) * ratio;
126                active.tick_interval *= ratio;
127            }
128
129            active.time_rate_multiplier = desired;
130        }
131
132        if let Some(active) = ctx.buf.aura(key) {
133            if active.expires_at < PERMANENT_AURA_EXPIRES_AT_S {
134                ctx.state.schedule(Event::AuraExpire {
135                    t: SimTime::from_secs_f64(active.expires_at),
136                    key,
137                    target: match key.affected() {
138                        ActorId::Enemy(enemy) => Some(enemy),
139                        _ => None,
140                    },
141                });
142            }
143
144            if active.tick_interval > 0.0 && active.next_tick <= active.expires_at {
145                ctx.state.schedule(Event::AuraTick {
146                    t: SimTime::from_secs_f64(active.next_tick),
147                    key,
148                    target: match key.affected() {
149                        ActorId::Enemy(enemy) => Some(enemy),
150                        _ => None,
151                    },
152                });
153            }
154        }
155    }
156}
157
158#[derive(Clone, Copy)]
159pub(crate) enum AuraBreakTrigger {
160    Action,
161    Damage { periodic: bool },
162}
163
164#[cfg(test)]
165#[path = "auras/tests.rs"]
166mod tests;