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, SpellIdx,
4    apply_cast_aura, deal_profiled_damage_def_at, execute_effect_range, fire_expire_hooks,
5    resolve_payload_flags,
6};
7
8pub(super) fn schedule_spell_impact(
9    ctx: &mut CombatCtx<'_>,
10    spell: &SpellData,
11    payload: SpellCastPayload,
12    now: SimTime,
13) {
14    let impact_id = u32::try_from(ctx.state.runtime.pending.pending_spell_impacts.len())
15        .expect("pending impact arena length fits in u32");
16
17    ctx.state
18        .runtime
19        .pending
20        .pending_spell_impacts
21        .push(Some(PendingSpellImpact {
22            spell_id: spell.spell_id,
23            damage_effect: spell.damage_effect,
24            base_points: payload.base_points,
25            damage: payload.damage,
26            is_pet: spell.behavior.is_pet,
27            may_crit: spell.damage_policy.may_crit,
28            attribute_flags: spell.damage_attribute_flags,
29            applies_aura: spell.applies_aura,
30            applies_aura_duration_ms: payload.aura_duration_ms,
31            followup_effects: spell.followup_effects,
32            source: ctx.source,
33            target: ctx.target,
34            cast_complete_at: now,
35            travel: spell.travel,
36            locked_source_transform: None,
37            locked_target_transform: None,
38            cast_destination: None,
39        }));
40
41    if let Some(slot) = ctx.buf.spell_mut(SpellIdx::from_raw(spell.spell_id)) {
42        slot.in_flight_count += 1;
43        slot.is_in_flight = 1;
44    }
45
46    if spell.travel.is_fixed() {
47        process_spell_launch(ctx, impact_id);
48    } else {
49        let launch_at = now.saturating_add(SimTime::from_millis(crate::travel::launch_offset_ms(
50            spell.travel,
51        )));
52
53        ctx.state.schedule(Event::SpellLaunch {
54            t: launch_at,
55            impact_id,
56            source: ctx.source,
57            target: ctx.target,
58        });
59    }
60}
61
62pub(crate) fn process_spell_launch(ctx: &mut CombatCtx<'_>, impact_id: u32) {
63    let Some((spell_id, source, target, cast_complete_at, travel)) = ctx
64        .state
65        .runtime
66        .pending
67        .pending_spell_impacts
68        .get(impact_id as usize)
69        .and_then(Option::as_ref)
70        .map(|pending| {
71            (
72                pending.spell_id,
73                pending.source,
74                pending.target,
75                pending.cast_complete_at,
76                pending.travel,
77            )
78        })
79    else {
80        return;
81    };
82
83    if !ctx.state.is_valid_target(target) {
84        cancel_pending_spell(ctx, impact_id);
85
86        return;
87    }
88
89    let Some(source_transform) = ctx.state.actor_transform(source) else {
90        cancel_pending_spell(ctx, impact_id);
91
92        return;
93    };
94    let Some(target_transform) = ctx.state.actor_transform(ActorId::Enemy(target)) else {
95        cancel_pending_spell(ctx, impact_id);
96
97        return;
98    };
99    let distance = source_transform
100        .position
101        .distance(target_transform.position);
102    let impact_at = cast_complete_at.saturating_add(SimTime::from_millis(
103        crate::travel::impact_offset_ms(travel, distance),
104    ));
105    let required_targets = SpellCastTargetFlags::from_bits_retain(
106        ctx.state
107            .config
108            .game_data
109            .required_explicit_target_mask(SpellIdx::from_raw(spell_id))
110            .unwrap_or_default(),
111    );
112    let cast_destination = required_targets
113        .contains(SpellCastTargetFlags::DEST_LOCATION)
114        .then_some(target_transform);
115
116    let Some(pending) = ctx
117        .state
118        .runtime
119        .pending
120        .pending_spell_impacts
121        .get_mut(impact_id as usize)
122        .and_then(Option::as_mut)
123    else {
124        return;
125    };
126
127    pending.locked_source_transform = Some(source_transform);
128    pending.locked_target_transform = Some(target_transform);
129    pending.cast_destination = cast_destination;
130    ctx.state.schedule(Event::SpellImpact {
131        t: impact_at,
132        impact_id,
133        source,
134        target,
135    });
136
137    if let Some(slot) = ctx.buf.spell_mut(SpellIdx::from_raw(spell_id)) {
138        slot.in_flight_end = slot.in_flight_end.max(impact_at.as_secs_f64());
139    }
140}
141
142pub(crate) fn process_spell_impact(ctx: &mut CombatCtx<'_>, impact_id: u32) {
143    let pending = ctx
144        .state
145        .runtime
146        .pending
147        .pending_spell_impacts
148        .get_mut(impact_id as usize)
149        .and_then(Option::take);
150    let Some(pending) = pending else {
151        return;
152    };
153
154    if !ctx.state.is_valid_target(pending.target)
155        || pending.locked_source_transform.is_none()
156        || pending.locked_target_transform.is_none()
157    {
158        finish_pending_spell_slot(ctx, &pending);
159
160        return;
161    }
162
163    ctx.source = pending.source;
164    ctx.target = pending.target;
165
166    let base_impact_flags = pending.attribute_flags
167        | (if pending.is_pet {
168            DamageFlags::PET
169        } else {
170            DamageFlags::empty()
171        })
172        | if pending.may_crit {
173            DamageFlags::empty()
174        } else {
175            DamageFlags::NO_CRIT
176        };
177    let Some(impact_flags) = resolve_payload_flags(
178        ctx,
179        pending.spell_id,
180        pending.damage,
181        pending.applies_aura,
182        pending.followup_effects,
183        base_impact_flags,
184    ) else {
185        finish_pending_spell_slot(ctx, &pending);
186
187        return;
188    };
189
190    let aura_applied_before_payload = pending.applies_aura.filter(|aura_local| {
191        pending.followup_effects.len > 0
192            && ctx
193                .state
194                .defs
195                .auras
196                .get(aura_local.as_usize())
197                .is_some_and(|aura| aura.on == wowlab_types::sim::AuraOn::Player)
198    });
199
200    if let Some(aura_local) = aura_applied_before_payload {
201        apply_cast_aura(
202            ctx,
203            aura_local,
204            pending.applies_aura_duration_ms,
205            impact_flags,
206        );
207    }
208
209    if !matches!(pending.damage, RuntimeDamageDef::None) {
210        deal_profiled_damage_def_at(
211            ctx,
212            &ProfiledDamageDef {
213                effect: pending.damage_effect,
214                geometry_effect: pending.damage_effect,
215                profile_spell_id: pending.spell_id,
216                base_points: Some(pending.base_points),
217                def: pending.damage,
218                flags: impact_flags,
219                source_position: pending.locked_source_transform,
220                destination: pending.cast_destination,
221            },
222        );
223    }
224
225    if let Some(aura_local) = pending
226        .applies_aura
227        .filter(|aura_local| Some(*aura_local) != aura_applied_before_payload)
228    {
229        apply_cast_aura(
230            ctx,
231            aura_local,
232            pending.applies_aura_duration_ms,
233            impact_flags,
234        );
235    }
236
237    execute_effect_range(
238        ctx,
239        EffectExecution {
240            range: pending.followup_effects,
241            profile_spell_id: pending.spell_id,
242            flags: impact_flags,
243        },
244    );
245
246    finish_pending_spell_slot(ctx, &pending);
247    // Same-time wake lets usable-while-casting actions react to impact state changes.
248    ctx.state.schedule(Event::OffGcdReady { t: ctx.now });
249    fire_expire_hooks(ctx);
250}
251
252pub(super) fn cancel_pending_spell(ctx: &mut CombatCtx<'_>, impact_id: u32) {
253    let pending = ctx
254        .state
255        .runtime
256        .pending
257        .pending_spell_impacts
258        .get_mut(impact_id as usize)
259        .and_then(Option::take);
260
261    if let Some(pending) = pending {
262        finish_pending_spell_slot(ctx, &pending);
263    }
264}
265
266pub(super) fn finish_pending_spell_slot(ctx: &mut CombatCtx<'_>, pending: &PendingSpellImpact) {
267    if let Some(slot) = ctx.buf.spell_mut(SpellIdx::from_raw(pending.spell_id)) {
268        slot.in_flight_count = (slot.in_flight_count - 1).max(0);
269
270        if slot.in_flight_count == 0 {
271            slot.is_in_flight = 0;
272            slot.in_flight_end = 0.0;
273        }
274    }
275}