Skip to main content

wowlab_engine_combat/systems/
auras.rs

1//! Aura application, expiry, stacking, snapshotting, and exclusive-group handling.
2
3#[cfg(test)]
4use wowlab_engine_domain::dbc::SpellAuraInterruptFlags;
5use wowlab_engine_ports::Event;
6#[cfg(test)]
7use wowlab_types::sim::SpellIdx;
8use wowlab_types::{
9    constants::HUNDRED,
10    sim::{ActorId, AuraKey, EnemyIdx, SimTime},
11};
12
13use super::{
14    EffectExecution, async_stacks, attempt_weapon_enchants, execute_effect_range_for_actor,
15    execute_proc_effect_range_for_actor, fire_aura_application_procs,
16    process_health_threshold_application, procs,
17};
18use crate::{
19    context::HookCtx,
20    state::{
21        ActiveAbsorb, AuraMutation, AuraSchedule, BuffEffect, CombatInstance, LocalAuraIdx,
22        SpellGroup, SpellGroupRule,
23    },
24};
25
26pub(crate) mod apply;
27mod breaks;
28mod copy;
29mod deferred;
30pub(crate) mod expire;
31pub(crate) mod stacks;
32
33#[cfg(all(test, feature = "rotation-optimizations"))]
34use apply::aura_time_rate_is_unchanged;
35use apply::enqueue_aura_application_proc;
36#[cfg(test)]
37use apply::fixed_tick_periodic_duration_ms;
38#[cfg(test)]
39use apply::schedule_aura_expiry;
40pub(crate) use apply::{apply_aura, apply_aura_with_duration, apply_aura_with_rolling_multiplier};
41pub(crate) use breaks::{
42    break_auras_on_actor, break_auras_without_telemetry, break_stealth_if_active,
43};
44pub(crate) use copy::aura_remaining_ms;
45#[cfg(test)]
46pub(crate) use copy::copy_target_aura;
47pub(crate) use deferred::drain_deferred_work;
48pub(crate) use expire::{expire_aura, expire_auras_on_actor};
49use expire::{expire_aura_instance, refresh_absorbs};
50pub(crate) use stacks::{
51    add_aura_stack, aura_stacks, consume_aura, consume_aura_stack, consume_aura_stacks,
52    extend_aura, is_aura_active, reduce_aura,
53};
54#[cfg(test)]
55pub(crate) use stacks::{aura_stacks_by_id, take_aura_stacks};
56
57mod application_state;
58
59#[cfg(test)]
60pub(crate) use application_state::capture_snapshot_fixture;
61#[cfg(test)]
62use application_state::pandemic_carry;
63#[cfg(test)]
64use application_state::pandemic_carry_ms;
65#[cfg(test)]
66pub(crate) use application_state::reference_recompute_all_aura_consumers;
67use application_state::{
68    apply_exclusive_group_dependencies, aura_query_key_for, combine_rolling_multiplier,
69    context_aura_key, emit_aura_expire_event, initialize_or_revive_periodic, is_rolling_periodic,
70    refresh_snapshot, refreshed_stacks, restore_active_shapeshift_form, revive_tick_chain,
71    rolling_ticks_left, update_aura_slot,
72};
73pub(crate) use application_state::{
74    aura_key_for, aura_keys_for_actor_application, aura_keys_for_application, capture_snapshot_for,
75    emit_aura_apply_event, emit_aura_refresh_event, refresh_all_aura_projections,
76    refresh_aura_snapshot_multiplier, refresh_planned_aura_projection,
77};
78
79const MAX_DEFERRED_WORK_ITEMS: usize = 1_024;
80const MAX_AURA_APPLICATION_ACTORS: usize = 2;
81const PERMANENT_AURA_EXPIRES_AT_S: f64 = 1.0e12;
82
83/// Policy for moving a periodic aura between targets.
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85#[cfg(test)]
86#[non_exhaustive]
87pub(crate) enum DotCopyBehavior {
88    /// Start a new tick chain with the source's remaining duration.
89    Start,
90    /// Clone the source state without carrying destination duration.
91    CloneNoRefresh,
92}
93
94pub(crate) fn refresh_aura_time_rates_for(
95    program: &crate::state::CombatProgram,
96    instance: &mut CombatInstance,
97    now: SimTime,
98    bindings: &[crate::state::program::ConcreteAuraBinding],
99) {
100    for binding in bindings {
101        let key = binding.key;
102        let Some(slot) = program
103            .buffer_aura(instance, binding.local, key)
104            .copied()
105            .filter(|slot| slot.is_active(now.as_secs_f64()))
106        else {
107            continue;
108        };
109        // BOUNDS: planned entries carry locals from this exact finalized aura table.
110        let _aura = program.auras[binding.local.as_usize()];
111        let desired = crate::systems::buffs::aura_time_rate_multiplier(
112            crate::context::ActorView::new(program, instance, key.affected()),
113            binding.local,
114        );
115        let current = if slot.time_rate_multiplier > 0.0 {
116            slot.time_rate_multiplier
117        } else {
118            1.0
119        };
120        let ratio = desired / current;
121
122        if (ratio - 1.0).abs() <= f64::EPSILON {
123            continue;
124        }
125
126        let now_s = now.as_secs_f64();
127
128        let expires_at = if slot.expires_at < PERMANENT_AURA_EXPIRES_AT_S {
129            now_s + (slot.expires_at - now_s).max(0.0) * ratio
130        } else {
131            slot.expires_at
132        };
133        let (next_tick, tick_interval) = if slot.tick_interval > 0.0 {
134            (
135                now_s + (slot.next_tick - now_s).max(0.0) * ratio,
136                slot.tick_interval * ratio,
137            )
138        } else {
139            (slot.next_tick, slot.tick_interval)
140        };
141
142        program.mutate_aura(
143            instance,
144            *binding,
145            AuraMutation::SetTiming {
146                expires_at,
147                schedule: AuraSchedule {
148                    next_tick,
149                    tick_interval,
150                    base_tick_interval: slot.base_tick_interval,
151                    time_rate_multiplier: desired,
152                    remaining_ticks: slot.remaining_ticks,
153                },
154            },
155            now,
156        );
157
158        if let Some(active) = program.buffer_aura(instance, binding.local, key).copied() {
159            if active.expires_at < PERMANENT_AURA_EXPIRES_AT_S {
160                instance.schedule(Event::AuraExpire {
161                    t: SimTime::from_secs_f64(active.expires_at),
162                    aura: program
163                        .aura_event(
164                            binding.local,
165                            key,
166                            match key.affected() {
167                                ActorId::Enemy(enemy) => Some(enemy),
168                                _ => None,
169                            },
170                        )
171                        .expect("active aura has a finalized event binding"),
172                });
173            }
174
175            if active.tick_interval > 0.0 && active.next_tick <= active.expires_at {
176                instance.schedule(Event::AuraTick {
177                    t: SimTime::from_secs_f64(active.next_tick),
178                    aura: program
179                        .aura_event(
180                            binding.local,
181                            key,
182                            match key.affected() {
183                                ActorId::Enemy(enemy) => Some(enemy),
184                                _ => None,
185                            },
186                        )
187                        .expect("periodic aura has a finalized event binding"),
188                });
189            }
190        }
191    }
192}
193
194#[derive(Clone, Copy)]
195pub(crate) enum AuraBreakTrigger {
196    Action,
197    Damage { periodic: bool },
198}
199
200#[cfg(test)]
201#[path = "auras/tests.rs"]
202mod tests;