1use super::{
4 ActorId, AuraKey, AuraOn, AuraTickBehavior, CombatCtx, CombatInstance, DamageFlags,
5 DamagePayload, DamageSnapshot, EffectExecution, EnemyIdx, Event, HUNDRED, HookCtx,
6 LocalAuraIdx, MIN_TICK_INTERVAL_S, MS_PER_SECOND, PeriodicKind, ResourceEventKind,
7 ResourceGain, ResourceGainSource, ResourceTelemetry, SimTime, SnapshotDamageInput,
8 SnapshotPower, TelemetrySink, apply_aura_system, aura_tick_due, deal_bound_damage_ap,
9 deal_bound_damage_sp, deal_damage_base, deal_damage_with_snapshot, deal_effect_damage_ap,
10 deal_effect_damage_base, deal_effect_damage_sp, drain_deferred_work,
11 effective_periodic_interval_ms, effective_periodic_tick_ms, emit_resource,
12 execute_effect_range, expire_aura, periodic_damage_snapshot, proc_chance, reapply_if_flagged,
13 spend_resource,
14};
15use crate::state::AuraMutation;
16
17struct PeriodicEffectContext<'a> {
18 program: &'a crate::state::CombatProgram,
19 instance: &'a mut CombatInstance,
20 sink: &'a mut TelemetrySink,
21 rng: &'a mut dyn FnMut() -> f64,
22 now: SimTime,
23 local: LocalAuraIdx,
24 spell_id: u32,
25 damage_binding: Option<crate::state::program::PeriodicDamageBinding>,
26 effect_ref: Option<crate::state::DamageEffectRef>,
27 may_crit: bool,
28 damage_is_periodic: bool,
29 key: AuraKey,
30 source: ActorId,
31 target: EnemyIdx,
32}
33
34impl PeriodicEffectContext<'_> {
35 fn aura_pool(&self) -> usize {
36 self.program
37 .aura_pool_index(self.local, self.key)
38 .expect("periodic aura has a finalized pool binding")
39 }
40 fn active_points(&self, initial: f64) -> f64 {
41 self.damage_binding.map_or(initial, |binding| {
42 crate::systems::buffs::active_effect_points(
43 crate::context::ActorView::new(self.program, self.instance, self.source),
44 binding.request.effect,
45 initial,
46 )
47 })
48 }
49
50 fn active_amplitude(&self, initial: f64) -> f64 {
51 self.damage_binding.map_or(initial, |binding| {
52 crate::systems::buffs::active_effect_amplitude(
53 crate::context::ActorView::new(self.program, self.instance, self.source),
54 binding.request.effect_action(),
55 initial,
56 )
57 })
58 }
59
60 fn damage_stack_multiplier(&self) -> f64 {
61 let Some(aura) = self.program.auras.get(self.local.as_usize()) else {
62 return 1.0;
63 };
64
65 if !aura.periodic_damage_scales_with_stacks {
66 return 1.0;
67 }
68
69 self.program
70 .buffer_aura(self.instance, self.local, self.key)
71 .map_or(1.0, |slot| f64::from(slot.stacks.max(1)))
72 }
73
74 fn combat_ctx(&mut self) -> CombatCtx<'_> {
75 CombatCtx {
76 program: self.program,
77 instance: self.instance,
78 sink: self.sink,
79 now: self.now,
80 rng: self.rng,
81 source: self.source,
82 target: self.target,
83 }
84 }
85
86 fn remaining_ticks(&self) -> f64 {
87 let (expires_at, tick_interval) = self
88 .program
89 .buffer_aura(self.instance, self.local, self.key)
90 .map_or((0.0, 0.0), |aura| (aura.expires_at, aura.tick_interval));
91
92 ((expires_at - self.now.as_secs_f64()) / tick_interval.max(MIN_TICK_INTERVAL_S))
93 .ceil()
94 .max(1.0)
95 }
96
97 fn classify_damage(&self, flags: DamageFlags) -> DamageFlags {
98 if self.damage_is_periodic {
99 flags | DamageFlags::PERIODIC
100 } else {
101 flags
102 }
103 }
104
105 fn damage_ap(
106 &mut self,
107 coef: f64,
108 flags: DamageFlags,
109 snapshot: Option<&DamageSnapshot>,
110 ) -> bool {
111 let spell_id = self.spell_id;
112 let effect = self.effect_ref;
113 let flags = if self.may_crit {
114 flags
115 } else {
116 flags | DamageFlags::NO_CRIT
117 };
118 let flags = self.classify_damage(flags);
119 let Some(binding) = self.damage_binding else {
120 return false;
121 };
122 let mut ctx = self.combat_ctx();
123
124 if let Some(snapshot) = snapshot {
125 deal_damage_with_snapshot(
126 &mut ctx,
127 SnapshotDamageInput {
128 binding: binding.request,
129 effect: effect
130 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 1)),
131 coefficient: coef,
132 flags,
133 power: SnapshotPower::Attack,
134 },
135 snapshot,
136 )
137 .1
138 } else if let Some(effect) = effect {
139 deal_effect_damage_ap(&mut ctx, binding.request, effect, coef, flags).1
140 } else {
141 deal_bound_damage_ap(
142 &mut ctx,
143 binding.request,
144 DamagePayload::new(spell_id, coef, flags),
145 )
146 .1
147 }
148 }
149
150 fn flat_damage(&mut self, amount: f64, is_physical: bool) -> bool {
151 let Some(binding) = self.damage_binding else {
152 return false;
153 };
154 let amount = amount * self.damage_stack_multiplier();
155 let spell_id = self.spell_id;
156 let mut flags = self.classify_damage(crate::systems::damage_flags_from_local(
157 self.program,
158 binding.request.effect_action(),
159 is_physical,
160 ));
161
162 if !self.may_crit {
163 flags |= DamageFlags::NO_CRIT;
164 }
165
166 if let Some(effect) = self.effect_ref {
167 deal_effect_damage_base(
168 &mut self.combat_ctx(),
169 binding.request,
170 effect,
171 spell_id,
172 amount,
173 flags,
174 )
175 .1
176 } else {
177 deal_damage_base(
178 &mut self.combat_ctx(),
179 binding.request,
180 spell_id,
181 amount,
182 flags,
183 )
184 .1
185 }
186 }
187
188 fn rolling_flat_damage(&mut self, amount: f64, is_physical: bool) -> bool {
189 let pool = self.aura_pool();
190 let multiplier = self.instance.runtime.pools.rolling_tick_mult.mult(pool);
191
192 self.flat_damage(amount * multiplier, is_physical)
193 }
194
195 fn rolling_damage_ap(&mut self, coef: f64, flags: DamageFlags) -> bool {
196 let pool = self.aura_pool();
197 let mult = self.instance.runtime.pools.rolling_tick_mult.mult(pool);
198
199 self.damage_ap(coef * mult, flags, None)
200 }
201
202 fn rolling_damage_sp(&mut self, coef: f64, flags: DamageFlags) -> bool {
203 let pool = self.aura_pool();
204 let mult = self.instance.runtime.pools.rolling_tick_mult.mult(pool);
205
206 self.damage_sp(coef * mult, flags, None)
207 }
208
209 fn damage_sp(
210 &mut self,
211 coef: f64,
212 flags: DamageFlags,
213 snapshot: Option<&DamageSnapshot>,
214 ) -> bool {
215 let spell_id = self.spell_id;
216 let effect = self.effect_ref;
217 let flags = if self.may_crit {
218 flags
219 } else {
220 flags | DamageFlags::NO_CRIT
221 };
222 let flags = self.classify_damage(flags);
223 let Some(binding) = self.damage_binding else {
224 return false;
225 };
226 let mut ctx = self.combat_ctx();
227
228 if let Some(snapshot) = snapshot {
229 deal_damage_with_snapshot(
230 &mut ctx,
231 SnapshotDamageInput {
232 binding: binding.request,
233 effect: effect
234 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 1)),
235 coefficient: coef,
236 flags,
237 power: SnapshotPower::Spell,
238 },
239 snapshot,
240 )
241 .1
242 } else if let Some(effect) = effect {
243 deal_effect_damage_sp(&mut ctx, binding.request, effect, coef, flags).1
244 } else {
245 deal_bound_damage_sp(
246 &mut ctx,
247 binding.request,
248 DamagePayload::new(spell_id, coef, flags),
249 )
250 .1
251 }
252 }
253
254 fn max_health_damage(&mut self, percent: f64, is_physical: bool) -> bool {
255 let Some(binding) = self.damage_binding else {
256 return false;
257 };
258 let Some(aura) = self.program.auras.get(self.local.as_usize()) else {
259 return false;
260 };
261
262 if aura.on != AuraOn::Target {
263 return false;
264 }
265
266 let Some(max_health) = self
267 .instance
268 .enemy_max_health(self.program, self.target)
269 .filter(|max_health| *max_health > 0.0)
270 else {
271 return false;
272 };
273 let amount =
274 max_health * self.active_points(percent) / HUNDRED * self.damage_stack_multiplier();
275 let mut flags = self.classify_damage(DamageFlags::empty());
276
277 if is_physical {
278 flags |= DamageFlags::PHYSICAL;
279 }
280
281 if !self.may_crit {
282 flags |= DamageFlags::NO_CRIT;
283 }
284
285 let spell_id = self.spell_id;
286
287 if let Some(effect) = self.effect_ref {
288 deal_effect_damage_base(
289 &mut self.combat_ctx(),
290 binding.request,
291 effect,
292 spell_id,
293 amount,
294 flags,
295 )
296 .1
297 } else {
298 deal_damage_base(
299 &mut self.combat_ctx(),
300 binding.request,
301 spell_id,
302 amount,
303 flags,
304 )
305 .1
306 }
307 }
308
309 fn heal(&mut self, base: f64, ap_coef: f64, sp_coef: f64, percent_of_max: f64) {
310 let base = self.active_points(base);
311 let percent_of_max = self.active_points(percent_of_max * HUNDRED) / HUNDRED;
312 let stats = &self.program.config.base_stats.stats;
313 let amount = base + ap_coef * stats.attack_power + sp_coef * stats.spell_power;
314 let target = match self.key.affected() {
315 ActorId::Enemy(_) => self.source,
316 actor => actor,
317 };
318 let _ = crate::systems::deal_heal(
319 self.program,
320 self.instance,
321 self.rng,
322 crate::systems::HealRequest {
323 source: self.source,
324 target,
325 base: amount,
326 percent_of_max,
327 school: wowlab_types::combat::DamageSchool::Holy,
328 periodic: true,
329 may_crit: self.may_crit,
330 at: self.now,
331 },
332 );
333 }
334
335 fn leech(
336 &mut self,
337 base: f64,
338 ap_coef: f64,
339 sp_coef: f64,
340 is_physical: bool,
341 heal_multiplier: f64,
342 ) -> bool {
343 let heal_multiplier = self.active_amplitude(heal_multiplier).max(0.0);
344 let mut flags = self.classify_damage(DamageFlags::empty());
345
346 if is_physical {
347 flags |= DamageFlags::PHYSICAL;
348 }
349
350 if !self.may_crit {
351 flags |= DamageFlags::NO_CRIT;
352 }
353
354 let spell_id = self.spell_id;
355 let Some(binding) = self.damage_binding else {
356 return false;
357 };
358 let (dealt, killed) = if ap_coef.abs() > f64::EPSILON {
359 deal_bound_damage_ap(
360 &mut self.combat_ctx(),
361 binding.request,
362 DamagePayload::new(spell_id, ap_coef, flags),
363 )
364 } else if sp_coef.abs() > f64::EPSILON {
365 deal_bound_damage_sp(
366 &mut self.combat_ctx(),
367 binding.request,
368 DamagePayload::new(spell_id, sp_coef, flags),
369 )
370 } else if let Some(effect) = self.effect_ref {
371 deal_effect_damage_base(
372 &mut self.combat_ctx(),
373 binding.request,
374 effect,
375 spell_id,
376 base,
377 flags,
378 )
379 } else {
380 deal_damage_base(
381 &mut self.combat_ctx(),
382 binding.request,
383 spell_id,
384 base,
385 flags,
386 )
387 };
388
389 self.heal(dealt * heal_multiplier, 0.0, 0.0, 0.0);
390
391 killed
392 }
393
394 fn gain_resource(
395 &mut self,
396 amount: f64,
397 resource_type: Option<wowlab_types::combat::ResourceType>,
398 ) {
399 let amount = self.active_points(amount);
400 let gain = ResourceGain::modified(amount, ResourceGainSource::Spell(self.spell_id));
401 let is_secondary = match resource_type {
402 Some(resource_type)
403 if Some(resource_type)
404 == self.program.config.base_stats.secondary_resource_type =>
405 {
406 true
407 }
408 Some(resource_type)
409 if Some(resource_type) != self.program.config.base_stats.resource_type =>
410 {
411 tracing::trace!(
412 ?resource_type,
413 "periodic energize targets an unavailable resource"
414 );
415
416 return;
417 }
418 _ => false,
419 };
420
421 crate::systems::gain_resource_with_procs(&mut self.combat_ctx(), gain, is_secondary);
422 }
423
424 fn drain_resource(&mut self, amount: f64) -> bool {
425 let amount = self.active_points(amount);
426
427 if spend_resource(self.program, self.instance, amount, self.now) {
428 emit_resource(
429 self.program,
430 self.instance,
431 self.sink,
432 ResourceTelemetry {
433 kind: ResourceEventKind::Spend,
434 amount,
435 secondary: false,
436 now: self.now,
437 source_spell_id: self.spell_id,
438 },
439 );
440
441 return true;
442 }
443
444 tracing::trace!(
445 aura_id = self.spell_id,
446 amount,
447 "aura expired: insufficient primary resource for drain tick"
448 );
449 expire_aura(
450 &mut HookCtx::new(
451 crate::context::HookCtxServices {
452 program: self.program,
453 instance: self.instance,
454 sink: self.sink,
455 rng: self.rng,
456 },
457 crate::context::HookCtxRequest::for_target(self.now, self.target)
458 .with_source(self.source),
459 ),
460 self.local,
461 );
462
463 false
464 }
465
466 fn hook_ctx(&mut self) -> HookCtx<'_> {
467 HookCtx::new(
468 crate::context::HookCtxServices {
469 program: self.program,
470 instance: self.instance,
471 sink: self.sink,
472 rng: self.rng,
473 },
474 crate::context::HookCtxRequest::for_target(self.now, self.target)
475 .with_source(self.source),
476 )
477 }
478
479 fn apply_aura(&mut self, target_aura_local: LocalAuraIdx) {
480 apply_aura_system(&mut self.hook_ctx(), target_aura_local);
481 }
482
483 fn add_aura_stack(&mut self, target_aura_local: LocalAuraIdx) {
484 crate::systems::add_aura_stack(&mut self.hook_ctx(), target_aura_local);
485 }
486
487 fn deal_residual_damage(&mut self) {
488 let remaining_ticks = self.remaining_ticks();
489 let pool = self.aura_pool();
490 let amount = self
491 .instance
492 .runtime
493 .pools
494 .residual_damage_pools
495 .take_tick(pool, remaining_ticks);
496 let spell_id = self.spell_id;
497 let Some(binding) = self.damage_binding.map(|binding| binding.request) else {
498 return;
499 };
500
501 crate::systems::damage_pipeline::deal_residual_damage(
502 &mut self.combat_ctx(),
503 binding,
504 spell_id,
505 amount,
506 );
507 }
508
509 fn remove_stack(&mut self) -> bool {
510 let depleted = self
511 .program
512 .buffer_aura(self.instance, self.local, self.key)
513 .map(|aura| (aura.stacks - 1).max(0))
514 .is_some_and(|stacks| {
515 if let Some(binding) = self.program.aura_binding(self.local, self.key) {
516 self.program.mutate_aura(
517 self.instance,
518 binding,
519 AuraMutation::SetStacks(stacks),
520 self.now,
521 );
522 }
523
524 stacks == 0
525 });
526
527 if depleted {
528 let mut ctx = HookCtx::new(
529 crate::context::HookCtxServices {
530 program: self.program,
531 instance: self.instance,
532 sink: self.sink,
533 rng: self.rng,
534 },
535 crate::context::HookCtxRequest::for_target(self.now, self.target)
536 .with_source(self.source),
537 );
538
539 expire_aura(&mut ctx, self.local);
540 reapply_if_flagged(&mut ctx, self.local);
541 }
542
543 !depleted
544 }
545}
546
547fn apply_periodic_effect(
548 ctx: &mut PeriodicEffectContext<'_>,
549 effect: PeriodicKind,
550 snapshot: Option<&DamageSnapshot>,
551) -> Option<bool> {
552 match effect {
553 PeriodicKind::FlatDamage {
554 amount,
555 is_physical,
556 } => Some(ctx.flat_damage(amount, is_physical)),
557 PeriodicKind::RollingFlatDamage {
558 amount,
559 is_physical,
560 } => Some(ctx.rolling_flat_damage(amount, is_physical)),
561 PeriodicKind::DamageAp { coef, is_physical } => {
562 let flags = periodic_damage_flags(is_physical);
563
564 Some(ctx.damage_ap(coef * ctx.damage_stack_multiplier(), flags, snapshot))
565 }
566 PeriodicKind::RollingDamageAp { coef, is_physical } => {
567 let flags = periodic_damage_flags(is_physical);
568
569 Some(ctx.rolling_damage_ap(coef * ctx.damage_stack_multiplier(), flags))
570 }
571 PeriodicKind::DamageSp { coef, is_physical } => {
572 let flags = periodic_damage_flags(is_physical);
573
574 Some(ctx.damage_sp(coef * ctx.damage_stack_multiplier(), flags, snapshot))
575 }
576 PeriodicKind::RollingDamageSp { coef, is_physical } => {
577 let flags = periodic_damage_flags(is_physical);
578
579 Some(ctx.rolling_damage_sp(coef * ctx.damage_stack_multiplier(), flags))
580 }
581 PeriodicKind::MaxHealthDamage {
582 percent,
583 is_physical,
584 } => Some(ctx.max_health_damage(percent, is_physical)),
585 PeriodicKind::Heal {
586 base,
587 ap_coef,
588 sp_coef,
589 percent_of_max,
590 } => {
591 ctx.heal(base, ap_coef, sp_coef, percent_of_max);
592
593 Some(false)
594 }
595 PeriodicKind::Leech {
596 base,
597 ap_coef,
598 sp_coef,
599 is_physical,
600 heal_multiplier,
601 } => Some(ctx.leech(base, ap_coef, sp_coef, is_physical, heal_multiplier)),
602 PeriodicKind::ResourceGain {
603 amount,
604 resource_type,
605 } => {
606 ctx.gain_resource(amount, resource_type);
607
608 Some(false)
609 }
610 PeriodicKind::ResourceDrain { amount } => ctx.drain_resource(amount).then_some(false),
611 PeriodicKind::ApplyAura { target_aura_local } => {
612 ctx.apply_aura(target_aura_local);
613
614 Some(false)
615 }
616 PeriodicKind::AddAuraStack { target_aura_local } => {
617 ctx.add_aura_stack(target_aura_local);
618
619 Some(false)
620 }
621 PeriodicKind::ResidualDamage => {
622 ctx.deal_residual_damage();
623
624 Some(false)
625 }
626 PeriodicKind::RemoveStack => ctx.remove_stack().then_some(false),
627 PeriodicKind::Hook => Some(false),
628 }
629}
630
631fn periodic_damage_flags(is_physical: bool) -> DamageFlags {
632 if is_physical {
633 DamageFlags::PHYSICAL
634 } else {
635 DamageFlags::empty()
636 }
637}
638
639pub(crate) fn periodic_tick_target(
640 program: &crate::state::CombatProgram,
641 state: &CombatInstance,
642 local: LocalAuraIdx,
643 key: AuraKey,
644 event_target: Option<EnemyIdx>,
645) -> Option<EnemyIdx> {
646 let aura = program.auras[local.as_usize()];
648 let event_target = event_target.or_else(|| match key.affected() {
649 ActorId::Enemy(enemy) => Some(enemy),
650 _ => None,
651 });
652
653 if aura.on == AuraOn::Player {
654 state.current_target()
655 } else {
656 event_target
657 }
658 .filter(|target| state.is_valid_target(*target))
659}
660
661pub fn process_single_aura_tick_for(ctx: &mut CombatCtx<'_>, local: LocalAuraIdx, key: AuraKey) {
663 tick_single_aura(ctx, local, key);
664 let mut hook_ctx = ctx.hook_ctx_with_source(key.source());
665
666 drain_deferred_work(&mut hook_ctx);
667}
668
669#[cfg(test)]
670pub(crate) fn process_single_aura_tick_fixture(ctx: &mut CombatCtx<'_>, aura_id: u32) {
671 let Some(local) = ctx.program.aura_local(aura_id) else {
672 return;
673 };
674 let Some(key) = crate::systems::auras::aura_key_for(
675 ctx.program,
676 ctx.instance,
677 local,
678 ctx.source,
679 Some(ctx.target),
680 ) else {
681 return;
682 };
683
684 process_single_aura_tick_for(ctx, local, key);
685}
686
687fn tick_single_aura(ctx: &mut CombatCtx<'_>, local: LocalAuraIdx, key: AuraKey) {
691 let now = ctx.now;
692 let target = ctx.target;
693 let program = ctx.program;
694 let state = &mut *ctx.instance;
695 let sink = &mut *ctx.sink;
696 let rng = &mut *ctx.rng;
697 let aura = program.auras[local.as_usize()];
699
700 if !aura_tick_due(program, state, local, key, now) {
701 return;
702 }
703
704 let Some(periodic) = aura.periodic else {
705 return;
706 };
707
708 let spell_id = periodic.damage_spell_id_or(aura.aura_id);
709
710 let snap = periodic_damage_snapshot(program, state, &aura, local, key, now);
711
712 let mut effect_ctx = PeriodicEffectContext {
713 program,
714 instance: state,
715 sink,
716 rng,
717 now,
718 local,
719 spell_id,
720 damage_binding: program.damage.periodic_request(local),
721 effect_ref: periodic.effect_ref,
722 may_crit: periodic.may_crit,
723 damage_is_periodic: periodic.damage_is_periodic,
724 key,
725 source: key.source(),
726 target,
727 };
728 let Some(crit) = apply_periodic_effect(&mut effect_ctx, periodic.effect, snap.as_ref()) else {
729 return;
730 };
731
732 if aura.tick_effects.len > 0 {
733 let profile_action = effect_ctx.program.damage.aura_tick_profile_action(local);
734
735 execute_effect_range(
736 &mut effect_ctx.combat_ctx(),
737 EffectExecution {
738 range: aura.tick_effects,
739 profile_spell_id: if aura.tick_profile_spell_id == 0 {
740 aura.aura_id
741 } else {
742 aura.tick_profile_spell_id
743 },
744 profile_action,
745 flags: DamageFlags::empty(),
746 },
747 );
748 }
749
750 if let Some(hook) = aura.on_tick {
753 let mut hook_ctx = HookCtx::new(
754 crate::context::HookCtxServices {
755 program,
756 instance: state,
757 sink,
758 rng,
759 },
760 crate::context::HookCtxRequest::for_target(now, target).with_source(key.source()),
761 )
762 .with_driver_spell(aura.aura_id);
763
764 hook(&mut hook_ctx);
765 }
766
767 let crit_gain = if crit
768 && periodic.crit_resource_gain > 0.0
769 && proc_chance(rng, periodic.crit_resource_chance)
770 {
771 periodic.crit_resource_gain
772 } else {
773 0.0
774 };
775 let resource_gain = periodic.resource_gain + crit_gain;
776
777 if resource_gain > 0.0 {
778 crate::systems::gain_resource_with_procs(
779 &mut CombatCtx {
780 program,
781 instance: state,
782 sink,
783 now,
784 rng,
785 source: key.source(),
786 target,
787 },
788 ResourceGain::modified(resource_gain, ResourceGainSource::Spell(spell_id)),
789 false,
790 );
791 }
792
793 if aura.tick_behavior != AuraTickBehavior::None && !aura.freeze_stacks {
794 let depleted = if let Some(slot) = program.buffer_aura(state, local, key) {
795 let change = i32::from(aura.tick_stack_change.max(1));
796
797 let stacks = if aura.reverse {
798 (slot.stacks - change).max(0)
799 } else {
800 (slot.stacks + change).min(slot.max_stacks)
801 };
802
803 if let Some(binding) = program.aura_binding(local, key) {
804 program.mutate_aura(state, binding, AuraMutation::SetStacks(stacks), now);
805 }
806
807 stacks == 0
808 } else {
809 false
810 };
811
812 if depleted {
813 expire_aura(
814 &mut HookCtx::new(
815 crate::context::HookCtxServices {
816 program,
817 instance: state,
818 sink,
819 rng,
820 },
821 crate::context::HookCtxRequest::for_target(now, target)
822 .with_source(key.source()),
823 ),
824 local,
825 );
826 let mut ctx = HookCtx::new(
827 crate::context::HookCtxServices {
828 program,
829 instance: state,
830 sink,
831 rng,
832 },
833 crate::context::HookCtxRequest::for_target(now, target).with_source(key.source()),
834 );
835
836 reapply_if_flagged(&mut ctx, local);
837
838 return;
839 }
840 }
841
842 let effective_tick_ms = effective_periodic_tick_ms(
843 &crate::context::CombatView::new(program, state),
844 local,
845 periodic.tick_interval_ms,
846 );
847 let base_s = f64::from(effective_tick_ms) / MS_PER_SECOND;
848 let next_interval_ms = effective_periodic_interval_ms(
849 &crate::context::CombatView::new(program, state),
850 local,
851 periodic.tick_interval_ms,
852 periodic.hasted_ticks,
853 );
854 let time_rate = program.buffer_aura(state, local, key).map_or(1.0, |slot| {
855 if slot.time_rate_multiplier > 0.0 {
856 slot.time_rate_multiplier
857 } else {
858 1.0
859 }
860 });
861 let interval_secs = f64::from(next_interval_ms) / MS_PER_SECOND * time_rate;
862
863 let next = SimTime::from_secs_f64(now.as_secs_f64() + interval_secs);
864 let next_time_s = next.as_secs_f64();
865 let expiry_s = program
866 .buffer_aura(state, local, key)
867 .map_or(0.0, |a| a.expires_at);
868
869 if next_time_s <= expiry_s {
870 if let Some(a) = program.buffer_aura(state, local, key).copied()
871 && let Some(binding) = program.aura_binding(local, key)
872 {
873 program.mutate_aura(
874 state,
875 binding,
876 AuraMutation::SetSchedule(crate::state::AuraSchedule {
877 base_tick_interval: base_s,
878 next_tick: next_time_s,
879 tick_interval: interval_secs,
880 time_rate_multiplier: a.time_rate_multiplier,
881 remaining_ticks: a.remaining_ticks,
882 }),
883 now,
884 );
885 }
886
887 state.schedule(Event::AuraTick {
888 t: next,
889 aura: program
890 .aura_event(local, key, Some(target))
891 .expect("periodic aura has a finalized event binding"),
892 });
893 }
894}