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