Skip to main content

wowlab_engine_combat/handler/spec_handler/
timers.rs

1use super::{
2    ActorId, CombatCtxRequest, CombatHandler, EnemyIdx, HookCtxRequest, SimContext, SimTime,
3    drain_deferred_work,
4};
5
6impl CombatHandler {
7    // #t(fn: rust_cyclomatic_complexity, rust_max_fn_lines) Timer validation, callback execution, rescheduling, and deferred-work draining form one causal transition.
8    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}