Skip to main content

wowlab_engine_combat/pipeline/
impact.rs

1use 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    // #t(rust_unchecked_indexing) impact_slot was obtained from or appended to this exact arena.
37    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
190// #t(rust_cyclomatic_complexity) validation, consumption, and damage dispatch are one causal transition.
191pub(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}