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(rust_cyclomatic_complexity) Timer dispatch keeps validation, callback execution, rescheduling, and deferred-work draining in one causal transition.
8    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}