wowlab_engine_combat/systems/
auras.rs1#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85#[cfg(test)]
86#[non_exhaustive]
87pub(crate) enum DotCopyBehavior {
88 Start,
90 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 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;