wowlab_engine_combat/pipeline/
impact.rs1use super::{
2 ActorId, CombatCtx, DamageFlags, EffectExecution, Event, PendingSpellImpact, ProfiledDamageDef,
3 RuntimeDamageDef, SimTime, SpellCastPayload, SpellCastTargetFlags, SpellData, apply_cast_aura,
4 deal_profiled_damage_def_at, execute_effect_range, fire_expire_hooks, resolve_payload_flags,
5};
6
7pub(super) fn schedule_spell_impact(
8 ctx: &mut CombatCtx<'_>,
9 spell_local: crate::state::LocalSpellIdx,
10 spell: &SpellData,
11 payload: SpellCastPayload,
12 now: SimTime,
13) {
14 let pending = &mut ctx.instance.runtime.pending;
15 let impact_slot = pending
16 .pending_spell_impacts
17 .iter()
18 .position(Option::is_none)
19 .unwrap_or_else(|| {
20 pending.pending_spell_impacts.push(None);
21 pending.spell_impact_generations.push(0);
22
23 pending.pending_spell_impacts.len() - 1
24 });
25 let generation = pending
26 .spell_impact_generations
27 .get_mut(impact_slot)
28 .expect("pending impact generation parallels its arena slot");
29
30 *generation = generation.wrapping_add(1).max(1);
31 let impact = ctx.program.runtime_events.pending_impact(
32 u32::try_from(impact_slot).expect("pending impact arena length fits in u32"),
33 *generation,
34 );
35
36 pending.pending_spell_impacts[impact_slot] = Some(PendingSpellImpact {
38 damage_binding: ctx.program.damage.spell_request(spell_local),
39 spell_local,
40 spell_handle: ctx.program.buffer_bindings.spell(spell_local),
41 spell_id: spell.spell_id,
42 profile_action: ctx.program.damage.spell_action(spell_local),
43 damage_effect: spell.damage_effect,
44 base_points: payload.base_points,
45 damage: payload.damage,
46 is_pet: spell.behavior.is_pet,
47 may_crit: spell.damage_policy.may_crit,
48 attribute_flags: spell.damage_attribute_flags,
49 applies_aura: spell.applies_aura,
50 applies_aura_duration_ms: payload.aura_duration_ms,
51 followup_effects: spell.followup_effects,
52 source: ctx.source,
53 target: ctx.target,
54 cast_complete_at: now,
55 travel: spell.travel,
56 required_explicit_target_mask: spell.targeting.required_explicit_target_mask,
57 locked_source_transform: None,
58 locked_target_transform: None,
59 cast_destination: None,
60 });
61
62 let slot = ctx
63 .instance
64 .buffer
65 .spell_mut_at(ctx.program.buffer_bindings.spell(spell_local));
66
67 slot.in_flight_count += 1;
68 slot.is_in_flight = 1;
69
70 #[cfg(feature = "rotation-optimizations")]
71 ctx.instance.runtime.rotation_mutations.record_semantic(
72 crate::state::runtime::RotationSemanticMutation::SpellState,
73 crate::state::runtime::RotationMutationDirection::Rise,
74 now,
75 );
76
77 if spell.travel.is_fixed() {
78 process_spell_launch(ctx, impact);
79 } else {
80 let launch_at = now.saturating_add(SimTime::from_millis(crate::travel::launch_offset_ms(
81 spell.travel,
82 )));
83
84 ctx.instance.schedule(Event::SpellLaunch {
85 t: launch_at,
86 impact,
87 source: ctx.source,
88 target: ctx.target,
89 });
90 }
91}
92
93pub(crate) fn process_spell_launch(
94 ctx: &mut CombatCtx<'_>,
95 impact: wowlab_engine_ports::PendingImpactBinding,
96) {
97 let Some((impact_slot, generation)) =
98 ctx.program.runtime_events.validate_pending_impact(impact)
99 else {
100 return;
101 };
102 let impact_slot = impact_slot as usize;
103
104 if ctx
105 .instance
106 .runtime
107 .pending
108 .spell_impact_generations
109 .get(impact_slot)
110 .copied()
111 != Some(generation)
112 {
113 return;
114 }
115
116 let Some((source, target, cast_complete_at, travel, target_mask, spell_handle)) = ctx
117 .instance
118 .runtime
119 .pending
120 .pending_spell_impacts
121 .get(impact_slot)
122 .and_then(Option::as_ref)
123 .map(|pending| {
124 (
125 pending.source,
126 pending.target,
127 pending.cast_complete_at,
128 pending.travel,
129 pending.required_explicit_target_mask,
130 pending.spell_handle,
131 )
132 })
133 else {
134 return;
135 };
136
137 if !ctx.instance.is_valid_target(target) {
138 cancel_pending_spell(ctx, impact);
139
140 return;
141 }
142
143 let Some(source_transform) = ctx.instance.actor_transform(source) else {
144 cancel_pending_spell(ctx, impact);
145
146 return;
147 };
148 let Some(target_transform) = ctx.instance.actor_transform(ActorId::Enemy(target)) else {
149 cancel_pending_spell(ctx, impact);
150
151 return;
152 };
153 let distance = source_transform
154 .position
155 .distance(target_transform.position);
156 let impact_at = cast_complete_at.saturating_add(SimTime::from_millis(
157 crate::travel::impact_offset_ms(travel, distance),
158 ));
159 let required_targets = SpellCastTargetFlags::from_bits_retain(target_mask);
160 let cast_destination = required_targets
161 .contains(SpellCastTargetFlags::DEST_LOCATION)
162 .then_some(target_transform);
163
164 let Some(pending) = ctx
165 .instance
166 .runtime
167 .pending
168 .pending_spell_impacts
169 .get_mut(impact_slot)
170 .and_then(Option::as_mut)
171 else {
172 return;
173 };
174
175 pending.locked_source_transform = Some(source_transform);
176 pending.locked_target_transform = Some(target_transform);
177 pending.cast_destination = cast_destination;
178 ctx.instance.schedule(Event::SpellImpact {
179 t: impact_at,
180 impact,
181 source,
182 target,
183 });
184
185 let slot = ctx.instance.buffer.spell_mut_at(spell_handle);
186
187 slot.in_flight_end = slot.in_flight_end.max(impact_at.as_secs_f64());
188}
189
190pub(crate) fn process_spell_impact(
192 ctx: &mut CombatCtx<'_>,
193 impact: wowlab_engine_ports::PendingImpactBinding,
194) {
195 let Some((impact_slot, generation)) =
196 ctx.program.runtime_events.validate_pending_impact(impact)
197 else {
198 return;
199 };
200 let impact_slot = impact_slot as usize;
201
202 if ctx
203 .instance
204 .runtime
205 .pending
206 .spell_impact_generations
207 .get(impact_slot)
208 .copied()
209 != Some(generation)
210 {
211 return;
212 }
213
214 let pending = ctx
215 .instance
216 .runtime
217 .pending
218 .pending_spell_impacts
219 .get_mut(impact_slot)
220 .and_then(Option::take);
221 let Some(pending) = pending else {
222 return;
223 };
224
225 if !ctx.instance.is_valid_target(pending.target)
226 || pending.locked_source_transform.is_none()
227 || pending.locked_target_transform.is_none()
228 {
229 finish_pending_spell_slot(ctx, &pending);
230
231 return;
232 }
233
234 ctx.source = pending.source;
235 ctx.target = pending.target;
236
237 let base_impact_flags = pending.attribute_flags
238 | (if pending.is_pet {
239 DamageFlags::PET
240 } else {
241 DamageFlags::empty()
242 })
243 | if pending.may_crit {
244 DamageFlags::empty()
245 } else {
246 DamageFlags::NO_CRIT
247 };
248 let Some(impact_flags) = resolve_payload_flags(
249 ctx,
250 pending.profile_action,
251 pending.damage,
252 pending.applies_aura,
253 pending.followup_effects,
254 base_impact_flags,
255 ) else {
256 finish_pending_spell_slot(ctx, &pending);
257
258 return;
259 };
260
261 let aura_applied_before_payload = pending.applies_aura.filter(|aura_local| {
262 pending.followup_effects.len > 0
263 && ctx
264 .program
265 .auras
266 .get(aura_local.as_usize())
267 .is_some_and(|aura| aura.on == wowlab_types::sim::AuraOn::Player)
268 });
269
270 if let Some(aura_local) = aura_applied_before_payload {
271 apply_cast_aura(
272 ctx,
273 aura_local,
274 pending.applies_aura_duration_ms,
275 impact_flags,
276 );
277 }
278
279 if !matches!(pending.damage, RuntimeDamageDef::None) {
280 deal_profiled_damage_def_at(
281 ctx,
282 &ProfiledDamageDef {
283 binding: pending
284 .damage_binding
285 .expect("damage-bearing pending impact was validated during finalization")
286 .request,
287 effect: pending.damage_effect,
288 geometry_effect: pending.damage_effect,
289 profile_spell_id: pending.spell_id,
290 base_points: Some(pending.base_points),
291 def: pending.damage,
292 flags: impact_flags,
293 source_position: pending.locked_source_transform,
294 destination: pending.cast_destination,
295 },
296 );
297 }
298
299 if let Some(aura_local) = pending
300 .applies_aura
301 .filter(|aura_local| Some(*aura_local) != aura_applied_before_payload)
302 {
303 apply_cast_aura(
304 ctx,
305 aura_local,
306 pending.applies_aura_duration_ms,
307 impact_flags,
308 );
309 }
310
311 execute_effect_range(
312 ctx,
313 EffectExecution {
314 range: pending.followup_effects,
315 profile_spell_id: pending.spell_id,
316 profile_action: pending.profile_action,
317 flags: impact_flags,
318 },
319 );
320
321 finish_pending_spell_slot(ctx, &pending);
322
323 #[cfg(not(feature = "rotation-optimizations"))]
324 {
325 ctx.instance.schedule(Event::OffGcdReady { t: ctx.now });
326 };
327
328 fire_expire_hooks(ctx);
329}
330
331pub(super) fn cancel_pending_spell(
332 ctx: &mut CombatCtx<'_>,
333 impact: wowlab_engine_ports::PendingImpactBinding,
334) {
335 let Some((impact_slot, generation)) =
336 ctx.program.runtime_events.validate_pending_impact(impact)
337 else {
338 return;
339 };
340 let impact_slot = impact_slot as usize;
341
342 if ctx
343 .instance
344 .runtime
345 .pending
346 .spell_impact_generations
347 .get(impact_slot)
348 .copied()
349 != Some(generation)
350 {
351 return;
352 }
353
354 let pending = ctx
355 .instance
356 .runtime
357 .pending
358 .pending_spell_impacts
359 .get_mut(impact_slot)
360 .and_then(Option::take);
361
362 if let Some(pending) = pending {
363 finish_pending_spell_slot(ctx, &pending);
364 }
365}
366
367pub(super) fn finish_pending_spell_slot(ctx: &mut CombatCtx<'_>, pending: &PendingSpellImpact) {
368 debug_assert_eq!(
369 pending.spell_handle,
370 ctx.program.buffer_bindings.spell(pending.spell_local),
371 "pending spell impact handle does not match its finalized local identity"
372 );
373 let changed = {
374 let slot = ctx.instance.buffer.spell_mut_at(pending.spell_handle);
375 let before = slot.in_flight_count;
376
377 slot.in_flight_count = (slot.in_flight_count - 1).max(0);
378
379 if slot.in_flight_count == 0 {
380 slot.is_in_flight = 0;
381 slot.in_flight_end = 0.0;
382 }
383
384 slot.in_flight_count != before
385 };
386
387 #[cfg(feature = "rotation-optimizations")]
388 if changed {
389 ctx.instance.runtime.rotation_mutations.record_semantic(
390 crate::state::runtime::RotationSemanticMutation::SpellState,
391 crate::state::runtime::RotationMutationDirection::Fall,
392 ctx.now,
393 );
394 }
395
396 #[cfg(not(feature = "rotation-optimizations"))]
397 let _ = changed;
398}