Skip to main content

wowlab_engine_combat/context/hook/
ground_effects.rs

1use wowlab_types::sim::{ActorId, EnemyIdx};
2
3use super::SchedulingOps;
4use crate::{
5    context::{FreeActionSource, HookCtx},
6    systems,
7};
8
9impl HookCtx<'_> {
10    pub(crate) fn schedule_periodic_driver(
11        &mut self,
12        delay_ms: u32,
13        driver_index: usize,
14        expires_at: Option<wowlab_types::sim::SimTime>,
15        target: Option<EnemyIdx>,
16    ) {
17        let pending = crate::state::PendingHookTimer {
18            action: crate::state::PendingTimerAction::PeriodicDriver {
19                index: driver_index,
20                expires_at,
21            },
22            source: ActorId::Player,
23            target,
24            source_damage_flags: crate::DamageFlags::empty(),
25            source_npc_id: None,
26            pet_stat_inheritance: crate::state::PetStatInheritance::IDENTITY,
27        };
28
29        self.schedule_pending_timer(delay_ms, pending);
30    }
31
32    pub(crate) fn schedule_effect_program(
33        &mut self,
34        delay_ms: u32,
35        effects: crate::state::SpellEffectRange,
36        profile_action: crate::state::program::LocalDamageActionIdx,
37        flags: crate::DamageFlags,
38        originating_proc_driver_spell_id: u32,
39    ) {
40        let target = self.target.or_else(|| self.instance.current_target());
41        let profile_spell_id = self.program.damage.action_at(profile_action).spell.as_u32();
42        let pending = crate::state::PendingHookTimer {
43            action: crate::state::PendingTimerAction::Effect(crate::state::PendingEffectProgram {
44                effects,
45                profile_spell_id,
46                profile_action,
47                repeat: 1,
48                source: self.source,
49                target,
50                effect_target: self.effect_target,
51                flags,
52                originating_proc_driver_spell_id,
53            }),
54            source: self.source,
55            target,
56            source_damage_flags: flags,
57            source_npc_id: self.source_npc_id,
58            pet_stat_inheritance: self.pet_stat_inheritance,
59        };
60
61        tracing::trace!(
62            delay_ms,
63            profile_spell_id,
64            effects_start = effects.start,
65            effects_len = effects.len,
66            "scheduled typed effect program"
67        );
68        self.schedule_pending_timer(delay_ms, pending);
69    }
70}
71
72impl SchedulingOps for HookCtx<'_> {
73    /// Execute a registered spell as a cost-free child action on this hook's locked target.
74    ///
75    /// The child retains its payload, aura/effects, gains, telemetry, and spell hook.
76    /// It bypasses player costs, cooldowns, GCD/history, and global player-cast hooks.
77    fn dispatch_free_action(
78        &mut self,
79        spell: crate::LocalSpellIdx,
80        source: FreeActionSource,
81    ) -> bool {
82        let spell = self.spell_identity(spell);
83        let Some(target) = self.target else {
84            return false;
85        };
86        let mut combat = self.combat_ctx(target);
87
88        crate::pipeline::process_free_action(&mut combat, spell, source)
89    }
90
91    /// Execute a registered spell spawned by a proc without player action costs.
92    ///
93    /// The originating driver is retained for proc provenance and self-loop suppression.
94    fn dispatch_proc_spell(
95        &mut self,
96        spell: crate::LocalSpellIdx,
97        proc_driver: crate::RppmBinding,
98        affected: ActorId,
99    ) -> bool {
100        let spell = self.spell_identity(spell);
101        let proc_driver_spell_id = crate::generated_identities::items::RPPM_DRIVERS
102            .get(proc_driver.as_usize())
103            .copied()
104            .unwrap_or(0);
105
106        if proc_driver_spell_id == 0 || self.program.spells.get(spell.as_usize()).is_none() {
107            return false;
108        }
109
110        let target = match affected {
111            ActorId::Enemy(target) => target,
112            ActorId::Player | ActorId::Pet(_) | ActorId::External => self
113                .target
114                .or_else(|| self.instance.current_target())
115                .unwrap_or(EnemyIdx::PRIMARY),
116        };
117        let mut combat = self.combat_ctx(target);
118
119        crate::pipeline::process_proc_triggered_spell(
120            &mut combat,
121            spell,
122            affected,
123            proc_driver_spell_id,
124        );
125
126        true
127    }
128
129    /// Schedule a hook for delayed execution; a captured target binds the callback and cancels it if that enemy is missing, inactive, or dead when the timer fires.
130    fn schedule_hook(&mut self, delay_ms: u32, hook: crate::state::CastHookFn) {
131        let pending = crate::state::PendingHookTimer {
132            action: crate::state::PendingTimerAction::Hook(hook),
133            source: self.source,
134            target: self.target,
135            source_damage_flags: self.source_damage_flags,
136            source_npc_id: self.source_npc_id,
137            pet_stat_inheritance: self.pet_stat_inheritance,
138        };
139
140        self.schedule_pending_timer(delay_ms, pending);
141    }
142
143    /// Activate a finite periodic driver by its real spell/passive identity.
144    fn trigger_periodic_driver(&mut self, driver: crate::PeriodicDriverBinding) -> bool {
145        let Some(index) = self
146            .program
147            .periodic_driver_bindings
148            .get(driver.as_usize())
149            .copied()
150            .flatten()
151        else {
152            return false;
153        };
154        let Some(duration_ms) = self
155            .program
156            .periodic_drivers
157            .get(index)
158            .and_then(|driver| driver.triggered_duration_ms)
159        else {
160            return false;
161        };
162
163        self.trigger_periodic_driver_for(driver, duration_ms.get())
164    }
165
166    /// Activate a finite periodic driver with a runtime-resolved lifetime.
167    ///
168    /// The driver must be registered as triggered.
169    /// This overrides the activation lifetime for mechanics whose duration varies per cast.
170    fn trigger_periodic_driver_for(
171        &mut self,
172        driver: crate::PeriodicDriverBinding,
173        duration_ms: u32,
174    ) -> bool {
175        let Some(duration_ms) = std::num::NonZeroU32::new(duration_ms) else {
176            return false;
177        };
178        let Some(index) = self
179            .program
180            .periodic_driver_bindings
181            .get(driver.as_usize())
182            .copied()
183            .flatten()
184        else {
185            return false;
186        };
187        let Some(driver) = self.program.periodic_drivers.get(index).copied() else {
188            return false;
189        };
190
191        if driver.triggered_duration_ms.is_none() {
192            return false;
193        }
194
195        let interval_ms = systems::effective_periodic_interval_for_action(
196            &self.view().combat(),
197            self.program.damage.periodic_driver_action(index),
198            driver.tick_interval_ms.get(),
199            driver.hasted_ticks,
200        );
201        let expires_at = self
202            .now
203            .saturating_add(wowlab_types::sim::SimTime::from_millis(duration_ms.get()));
204
205        if self
206            .now
207            .saturating_add(wowlab_types::sim::SimTime::from_millis(interval_ms))
208            > expires_at
209        {
210            return false;
211        }
212
213        self.schedule_periodic_driver(interval_ms, index, Some(expires_at), self.target);
214
215        true
216    }
217
218    /// Schedule a location-locked sequence of spatial damage pulses.
219    fn schedule_ground_effect(&mut self, effect: crate::GroundEffectId) -> bool {
220        let Some(plan) = self.program.ground_effects.get(effect.index()).copied() else {
221            return false;
222        };
223
224        let Some(destination) = self
225            .target
226            .and_then(|target| self.instance.actor_transform(ActorId::Enemy(target)))
227        else {
228            tracing::warn!(
229                spell_id = plan.profile_spell_id,
230                "ground effect requires a cast destination"
231            );
232
233            return false;
234        };
235        let haste = systems::current_haste_mult(&self.view().combat());
236        let duration_ms = if plan.duration_hasted {
237            wowlab_types::numeric::f64_to_u32_saturating_round(
238                f64::from(plan.base_duration_ms) / haste,
239            )
240        } else {
241            plan.base_duration_ms
242        };
243        let period_ms = if plan.period_hasted {
244            wowlab_types::numeric::f64_to_u32_saturating_round(
245                f64::from(plan.base_period_ms) / haste,
246            )
247        } else {
248            plan.base_period_ms
249        };
250        let pulse_count = if plan.pulses_from_duration {
251            duration_ms
252                .div_ceil(period_ms.max(1))
253                .min(u32::from(plan.pulse_bound))
254        } else {
255            u32::from(plan.pulse_bound)
256        };
257        let flags = self.source_flags(plan.flags);
258
259        for pulse in 0..pulse_count {
260            let delay_ms = plan
261                .first_pulse_delay_ms
262                .saturating_add(period_ms.saturating_mul(pulse));
263
264            self.schedule_pending_timer(
265                delay_ms,
266                crate::state::PendingHookTimer {
267                    action: crate::state::PendingTimerAction::GroundPulse(
268                        crate::state::PendingGroundPulse {
269                            binding: plan.binding,
270                            geometry_effect: plan.geometry_effect,
271                            damage_effect: plan.damage_effect,
272                            profile_spell_id: plan.profile_spell_id,
273                            damage: plan.damage,
274                            destination,
275                            flags,
276                        },
277                    ),
278                    source: self.source,
279                    target: None,
280                    source_damage_flags: self.source_damage_flags,
281                    source_npc_id: self.source_npc_id,
282                    pet_stat_inheritance: self.pet_stat_inheritance,
283                },
284            );
285        }
286
287        true
288    }
289}