Skip to main content

wowlab_engine_combat/pipeline/
cast.rs

1use super::{
2    CombatCtx, DamageFlags, LocalSpellIdx, ProfiledDamageDef, RuntimeDamageDef, SpellCastPayload,
3    SpellData, adjust_empower_payload, apply_cast_aura, break_stealth_if_active,
4    consume_cast_auras, deal_bound_profiled_damage_def, effective_gcd_ms, emit_cast_telemetry,
5    empower_rank_for_cast, empower_release_adjustment, finisher_scaled_damage, fire_cast_hook,
6    fire_expire_hooks, fire_player_cast_hooks, fire_spell_phase_procs,
7    player_aura_for_triggered_payload, process_cdr_effects, process_channel_cast,
8    process_followup_effects, process_resource_threshold_triggers, process_resources,
9    resolve_payload_flags, schedule_ready_after_cast, schedule_spell_impact,
10    start_cooldown_in_slot, update_history,
11};
12
13#[derive(Clone, Copy)]
14struct ResolvedPlayerCast {
15    spell_local: LocalSpellIdx,
16    spell: SpellData,
17    empower_rank: u8,
18    cooldown_bypassed: bool,
19    override_aura_to_consume: Option<crate::state::LocalAuraIdx>,
20    cooldown_aura_to_consume: Option<crate::state::LocalAuraIdx>,
21    consume_aura_on_cast: Option<crate::state::LocalAuraIdx>,
22}
23
24fn resolve_player_cast(
25    ctx: &mut CombatCtx<'_>,
26    candidate: crate::handler::can_cast::CastCandidate,
27    requested_empower_rank: u8,
28) -> ResolvedPlayerCast {
29    let spell_local = candidate.effective_local;
30    let spell = *candidate.effective(ctx.program);
31    let profile = candidate.effective_profile(ctx.program);
32    let overridden = candidate.effective_local != candidate.requested_local;
33    let empower_rank = empower_rank_for_cast(ctx, spell_local, requested_empower_rank);
34    let cooldown_bypassed = profile.cooldown_bypass_aura.is_some_and(|requirement| {
35        ctx.program
36            .rotation
37            .aura_active(ctx.instance, requirement, ctx.target)
38    });
39    let override_aura_to_consume = overridden
40        .then_some(candidate.override_aura_to_consume)
41        .flatten();
42    let cooldown_aura_to_consume = cooldown_bypassed
43        .then_some(profile.cooldown_bypass_aura)
44        .flatten()
45        .map(|requirement| requirement.local);
46    let consume_aura_on_cast = profile
47        .consume_aura_on_cast
48        .map(|requirement| requirement.local);
49
50    ResolvedPlayerCast {
51        spell_local,
52        spell,
53        empower_rank,
54        cooldown_bypassed,
55        override_aura_to_consume,
56        cooldown_aura_to_consume,
57        consume_aura_on_cast,
58    }
59}
60
61fn process_cast_impact(
62    ctx: &mut CombatCtx<'_>,
63    spell_local: LocalSpellIdx,
64    spell: &SpellData,
65    payload: SpellCastPayload,
66) {
67    let has_impact_payload = !matches!(payload.damage, RuntimeDamageDef::None)
68        || spell.applies_aura.is_some()
69        || spell.followup_effects.len > 0;
70
71    if spell.travel.has_projectile_timing() && has_impact_payload {
72        schedule_spell_impact(ctx, spell_local, spell, payload, ctx.now);
73
74        return;
75    }
76
77    let base_impact_flags = spell.damage_attribute_flags
78        | if spell.behavior.is_pet {
79            DamageFlags::PET
80        } else {
81            DamageFlags::empty()
82        }
83        | if spell.damage_policy.may_crit {
84            DamageFlags::empty()
85        } else {
86            DamageFlags::NO_CRIT
87        };
88    let Some(impact_flags) = resolve_payload_flags(
89        ctx,
90        ctx.program.damage.spell_action(spell_local),
91        payload.damage,
92        spell.applies_aura,
93        spell.followup_effects,
94        base_impact_flags,
95    ) else {
96        return;
97    };
98    let aura_applied_before_payload =
99        player_aura_for_triggered_payload(ctx.program, ctx.instance, spell);
100
101    if let Some(aura_local) = aura_applied_before_payload {
102        apply_cast_aura(ctx, aura_local, payload.aura_duration_ms, impact_flags);
103    }
104
105    if let Some(binding) = ctx.program.damage.spell_request(spell_local) {
106        deal_bound_profiled_damage_def(
107            ctx,
108            ProfiledDamageDef {
109                binding: binding.request,
110                effect: spell.damage_effect,
111                geometry_effect: spell.damage_effect,
112                profile_spell_id: spell.spell_id,
113                base_points: Some(payload.base_points),
114                def: payload.damage,
115                flags: impact_flags,
116                source_position: None,
117                destination: None,
118            },
119        );
120    }
121
122    if let Some(aura_local) = spell
123        .applies_aura
124        .filter(|aura_local| Some(*aura_local) != aura_applied_before_payload)
125    {
126        apply_cast_aura(ctx, aura_local, payload.aura_duration_ms, impact_flags);
127    }
128
129    process_followup_effects(ctx, spell_local, spell, impact_flags);
130}
131
132#[cfg(any(test, feature = "test-support"))]
133fn process_cast_impl(ctx: &mut CombatCtx<'_>, spell_id: u32, requested_empower_rank: u8) {
134    let Some(local) = ctx.program.spell_local(spell_id) else {
135        tracing::warn!(spell_id, "UNKNOWN_SPELL_CAST");
136
137        return;
138    };
139    let candidate = crate::handler::can_cast::CastCandidate::resolve_planned(
140        ctx.instance,
141        ctx.program.rotation.local_binding(local),
142        ctx.target,
143    );
144
145    process_selected_cast(ctx, candidate, requested_empower_rank);
146}
147
148#[cfg(any(test, feature = "test-support"))]
149#[doc(hidden)]
150pub(crate) fn process_cast(ctx: &mut CombatCtx<'_>, spell_id: u32, requested_empower_rank: u8) {
151    process_cast_impl(ctx, spell_id, requested_empower_rank);
152}
153
154pub(crate) fn process_selected_cast(
155    ctx: &mut CombatCtx<'_>,
156    candidate: crate::handler::can_cast::CastCandidate,
157    requested_empower_rank: u8,
158) {
159    let cast = resolve_player_cast(ctx, candidate, requested_empower_rank);
160    let ResolvedPlayerCast {
161        spell_local,
162        spell,
163        empower_rank,
164        cooldown_bypassed,
165        override_aura_to_consume,
166        cooldown_aura_to_consume,
167        consume_aura_on_cast,
168    } = cast;
169    let gcd_ms = effective_gcd_ms(&ctx.view(), spell_local, spell.base_gcd_ms());
170    let now = ctx.now;
171
172    emit_cast_telemetry(ctx, &spell, now, gcd_ms);
173    fire_spell_phase_procs(
174        ctx,
175        ctx.program.damage.spell_action(spell_local),
176        crate::state::ProcPhaseMask::CAST,
177    );
178
179    let profile = *candidate.effective_profile(ctx.program);
180
181    process_resources(ctx, &spell, &profile, now);
182    process_resource_threshold_triggers(ctx);
183
184    if spell.channel.behavior.is_channel {
185        process_channel_cast(
186            ctx,
187            candidate,
188            profile.channel_tick_cost_aura,
189            profile.channel_tick_damage_aura,
190            &spell,
191            empower_rank,
192        );
193        consume_cast_auras(
194            ctx,
195            override_aura_to_consume,
196            cooldown_aura_to_consume,
197            consume_aura_on_cast,
198        );
199        fire_spell_phase_procs(
200            ctx,
201            ctx.program.damage.spell_action(spell_local),
202            crate::state::ProcPhaseMask::FINISH,
203        );
204        fire_expire_hooks(ctx);
205
206        return;
207    }
208
209    schedule_ready_after_cast(ctx, &spell, now);
210
211    if spell.cooldown.has_cooldown && !cooldown_bypassed {
212        let cooldown_local = candidate.cooldown_local;
213        let cooldown_ms =
214            start_cooldown_in_slot(ctx.program, ctx.instance, spell_local, cooldown_local, now);
215
216        ctx.sink
217            .emit_cooldown_start(spell.spell_id, cooldown_ms, now.as_millis());
218    }
219
220    let adjustment = empower_release_adjustment(ctx, spell_local, empower_rank);
221    let payload = adjust_empower_payload(
222        ctx.program,
223        ctx.instance,
224        &spell,
225        finisher_scaled_damage(ctx, &spell),
226        adjustment,
227    );
228
229    process_cast_impact(ctx, spell_local, &spell, payload);
230
231    process_cdr_effects(ctx, &spell);
232
233    fire_cast_hook(ctx, spell_local, empower_rank);
234
235    fire_player_cast_hooks(ctx, spell.spell_id, empower_rank);
236
237    consume_cast_auras(
238        ctx,
239        override_aura_to_consume,
240        cooldown_aura_to_consume,
241        consume_aura_on_cast,
242    );
243
244    if spell.behavior.breaks_stealth {
245        break_stealth_if_active(&mut ctx.hook_ctx());
246    }
247
248    ctx.instance
249        .buffer
250        .spell_mut_at(candidate.effective_slot(ctx.program))
251        .last_used = now.as_secs_f64();
252
253    update_history(
254        ctx.instance,
255        candidate.effective_history(ctx.program),
256        spell.behavior.off_gcd,
257        now,
258    );
259
260    #[cfg(feature = "rotation-optimizations")]
261    ctx.instance.runtime.rotation_mutations.record_semantic(
262        crate::state::runtime::RotationSemanticMutation::SpellState,
263        crate::state::runtime::RotationMutationDirection::Rise,
264        now,
265    );
266
267    fire_spell_phase_procs(
268        ctx,
269        ctx.program.damage.spell_action(spell_local),
270        crate::state::ProcPhaseMask::FINISH,
271    );
272    fire_expire_hooks(ctx);
273}