wowlab_engine_combat/context/hook/
ground_effects.rs1use 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 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 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 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 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 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 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}