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 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
179pub fn apply_aura(ctx: &mut HookCtx<'_>, local: LocalAuraIdx) {
181 apply_aura_with_duration(ctx, local, None);
182}
183
184pub(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
193pub 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 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 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 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
368pub(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}