wowlab_engine_combat/handler/spec_handler/
timers.rs1use super::{
2 ActorId, CombatCtxRequest, CombatHandler, EnemyIdx, HookCtxRequest, SimContext, SimTime,
3 drain_deferred_work,
4};
5
6impl CombatHandler {
7 pub(super) fn on_hook_timer_impl(
9 &mut self,
10 timer_id: u32,
11 source: ActorId,
12 target: Option<EnemyIdx>,
13 ctx: &mut SimContext,
14 ) {
15 let now = ctx.state.current_time;
16 let Some(timer) = self
17 .state
18 .runtime
19 .pending
20 .pending_hook_timers
21 .get_mut(timer_id as usize)
22 else {
23 return;
24 };
25
26 if !timer
27 .as_ref()
28 .is_some_and(|pending| pending.source == source && pending.target == target)
29 {
30 return;
31 }
32
33 let Some(pending) = timer.take() else {
34 return;
35 };
36
37 match pending.action {
38 crate::state::PendingTimerAction::Hook(hook) => {
39 if pending
40 .target
41 .is_some_and(|target| !self.state.is_valid_target(target))
42 {
43 return;
44 }
45
46 self.with_hook_ctx(
47 &mut *ctx.telemetry,
48 HookCtxRequest::for_optional_target(now, pending.target)
49 .with_source(pending.source)
50 .with_source_damage_flags(pending.source_damage_flags)
51 .with_source_npc_id(pending.source_npc_id),
52 hook,
53 );
54 }
55 crate::state::PendingTimerAction::PeriodicDriver { index, expires_at } => {
56 let Some(driver) = self.state.defs.periodic_drivers.get(index).copied() else {
57 return;
58 };
59 let target = if expires_at.is_some() {
60 if pending
61 .target
62 .is_some_and(|target| !self.state.is_valid_target(target))
63 {
64 return;
65 }
66
67 pending.target
68 } else {
69 self.state.current_target()
70 };
71
72 self.with_hook_ctx(
73 &mut *ctx.telemetry,
74 HookCtxRequest::for_optional_target(now, target).with_source(ActorId::Player),
75 |hook_ctx| {
76 (driver.tick)(hook_ctx);
77
78 let interval_ms = crate::systems::effective_periodic_interval_ms(
79 &hook_ctx.view().combat(),
80 driver.driver_spell_id,
81 driver.tick_interval_ms.get(),
82 driver.hasted_ticks,
83 );
84
85 let next_tick = now.saturating_add(SimTime::from_millis(interval_ms));
86
87 if expires_at.is_none_or(|expires_at| next_tick <= expires_at) {
88 hook_ctx.schedule_periodic_driver(
89 interval_ms,
90 index,
91 expires_at,
92 pending.target,
93 );
94 }
95 },
96 );
97 }
98 crate::state::PendingTimerAction::GroundPulse(pulse) => {
99 if let Some(target) = crate::systems::ground_pulse_anchor(&self.state) {
100 self.with_combat_ctx(
101 &mut *ctx.telemetry,
102 CombatCtxRequest::new(now, pending.source, target),
103 |combat| crate::systems::process_ground_pulse(combat, pulse),
104 );
105 } else {
106 tracing::trace!(
107 spell_id = pulse.damage_effect.spell_id,
108 "ground-effect pulse skipped without an active hostile anchor"
109 );
110 }
111 }
112 crate::state::PendingTimerAction::Effect(program) => {
113 let target = program
114 .target
115 .or_else(|| self.state.current_target())
116 .unwrap_or(EnemyIdx::PRIMARY);
117
118 if matches!(program.effect_target, Some(ActorId::Enemy(_)))
119 && !self.state.is_valid_target(target)
120 {
121 return;
122 }
123
124 tracing::trace!(
125 profile_spell_id = program.profile_spell_id,
126 effects_start = program.effects.start,
127 effects_len = program.effects.len,
128 "executing scheduled typed effect program"
129 );
130 self.with_combat_ctx(
131 &mut *ctx.telemetry,
132 CombatCtxRequest::new(now, program.source, target),
133 |combat| {
134 crate::systems::execute_effect_range_for_actor(
135 combat,
136 crate::systems::EffectExecution {
137 range: program.effects,
138 profile_spell_id: program.profile_spell_id,
139 flags: program.flags,
140 },
141 program.effect_target,
142 );
143 },
144 );
145 }
146 }
147
148 self.with_hook_ctx(
149 &mut *ctx.telemetry,
150 HookCtxRequest::for_optional_target(now, pending.target).with_source(pending.source),
151 drain_deferred_work,
152 );
153 }
154}