Skip to main content

wowlab_engine_combat/pipeline/
cast.rs

1use super::{
2    CombatCtx, DamageFlags, LocalSpellIdx, RuntimeDamageDef, SpellCastPayload, SpellData, SpellIdx,
3    adjust_empower_payload, apply_cast_aura, aura_stacks_by_id, break_stealth_if_active,
4    bypass_aura_active, consume_cast_auras, deal_profiled_damage_def, effective_gcd_ms,
5    emit_cast_telemetry, empower_rank_for_cast, empower_release_adjustment, finisher_scaled_damage,
6    fire_cast_hook, 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    requested_spell: SpellData,
17    spell: SpellData,
18    overridden: bool,
19    empower_rank: u8,
20    cooldown_bypassed: bool,
21    override_aura_to_consume: Option<u32>,
22    cooldown_aura_to_consume: Option<u32>,
23}
24
25fn resolve_player_cast(
26    ctx: &mut CombatCtx<'_>,
27    spell_id: u32,
28    requested_empower_rank: u8,
29) -> Option<ResolvedPlayerCast> {
30    // docref:start cast-pipeline-lookup-guard
31    let Some((spell_local, spell_ref)) = ctx.state.spell_data(spell_id) else {
32        tracing::warn!(spell_id, "UNKNOWN_SPELL_CAST");
33        return None;
34    };
35    // docref:end cast-pipeline-lookup-guard
36    let requested_spell = *spell_ref;
37    let override_min_stacks = i32::from(requested_spell.overrides.aura_min_stacks.max(1));
38    let overridden = requested_spell.overrides.aura_id != 0
39        && requested_spell.overrides.spell_id != 0
40        && aura_stacks_by_id(
41            ctx.state,
42            ctx.buf,
43            requested_spell.overrides.aura_id,
44            ctx.source,
45            Some(ctx.target),
46        ) >= override_min_stacks;
47    let (spell_local, spell) = if overridden {
48        if let Some((local, replacement)) = ctx.state.spell_data(requested_spell.overrides.spell_id)
49        {
50            (local, *replacement)
51        } else {
52            tracing::warn!(
53                spell_id,
54                override_spell_id = requested_spell.overrides.spell_id,
55                "UNKNOWN_OVERRIDE_SPELL"
56            );
57
58            (spell_local, requested_spell)
59        }
60    } else {
61        (spell_local, requested_spell)
62    };
63    let empower_rank = empower_rank_for_cast(ctx, spell_local, requested_empower_rank);
64    let cooldown_bypassed = bypass_aura_active(
65        ctx.state,
66        ctx.buf,
67        spell.requirements.cooldown_bypass_aura_id,
68        ctx.source,
69        Some(ctx.target),
70    );
71    let override_aura_to_consume = (overridden && !requested_spell.overrides.preserves_aura)
72        .then_some(requested_spell.overrides.aura_id);
73    let cooldown_aura_to_consume =
74        cooldown_bypassed.then_some(spell.requirements.cooldown_bypass_aura_id);
75
76    Some(ResolvedPlayerCast {
77        spell_local,
78        requested_spell,
79        spell,
80        overridden,
81        empower_rank,
82        cooldown_bypassed,
83        override_aura_to_consume,
84        cooldown_aura_to_consume,
85    })
86}
87
88fn process_cast_impact(ctx: &mut CombatCtx<'_>, spell: &SpellData, payload: SpellCastPayload) {
89    let has_impact_payload = !matches!(payload.damage, RuntimeDamageDef::None)
90        || spell.applies_aura.is_some()
91        || spell.followup_effects.len > 0;
92
93    if spell.travel.has_projectile_timing() && has_impact_payload {
94        schedule_spell_impact(ctx, spell, payload, ctx.now);
95
96        return;
97    }
98
99    let base_impact_flags = spell.damage_attribute_flags
100        | if spell.behavior.is_pet {
101            DamageFlags::PET
102        } else {
103            DamageFlags::empty()
104        }
105        | if spell.damage_policy.may_crit {
106            DamageFlags::empty()
107        } else {
108            DamageFlags::NO_CRIT
109        };
110    let Some(impact_flags) = resolve_payload_flags(
111        ctx,
112        spell.spell_id,
113        payload.damage,
114        spell.applies_aura,
115        spell.followup_effects,
116        base_impact_flags,
117    ) else {
118        return;
119    };
120    let aura_applied_before_payload = player_aura_for_triggered_payload(ctx.state, spell);
121
122    if let Some(aura_local) = aura_applied_before_payload {
123        apply_cast_aura(ctx, aura_local, payload.aura_duration_ms, impact_flags);
124    }
125
126    deal_profiled_damage_def(
127        ctx,
128        spell.damage_effect,
129        spell.spell_id,
130        Some(payload.base_points),
131        payload.damage,
132        impact_flags,
133    );
134
135    if let Some(aura_local) = spell
136        .applies_aura
137        .filter(|aura_local| Some(*aura_local) != aura_applied_before_payload)
138    {
139        apply_cast_aura(ctx, aura_local, payload.aura_duration_ms, impact_flags);
140    }
141
142    process_followup_effects(ctx, spell, impact_flags);
143}
144
145pub fn process_cast(ctx: &mut CombatCtx<'_>, spell_id: u32, requested_empower_rank: u8) {
146    let Some(cast) = resolve_player_cast(ctx, spell_id, requested_empower_rank) else {
147        return;
148    };
149    let ResolvedPlayerCast {
150        spell_local,
151        requested_spell,
152        spell,
153        overridden,
154        empower_rank,
155        cooldown_bypassed,
156        override_aura_to_consume,
157        cooldown_aura_to_consume,
158    } = cast;
159    let gcd_ms = effective_gcd_ms(&ctx.view(), spell.spell_id, spell.base_gcd_ms());
160    let now = ctx.now;
161
162    emit_cast_telemetry(ctx, &spell, now, gcd_ms);
163    fire_spell_phase_procs(ctx, spell.spell_id, crate::state::ProcPhaseMask::CAST);
164
165    process_resources(ctx, &spell, now);
166    process_resource_threshold_triggers(ctx);
167
168    if spell.channel.behavior.is_channel {
169        let cooldown_local = if overridden && requested_spell.overrides.shares_base_cooldown {
170            ctx.state
171                .spell_local(requested_spell.spell_id)
172                .unwrap_or(spell_local)
173        } else {
174            spell_local
175        };
176
177        process_channel_cast(ctx, spell_local, cooldown_local, &spell, empower_rank, now);
178        consume_cast_auras(
179            ctx,
180            override_aura_to_consume,
181            cooldown_aura_to_consume,
182            spell.requirements.consume_aura_on_cast_id,
183        );
184        fire_spell_phase_procs(ctx, spell.spell_id, crate::state::ProcPhaseMask::FINISH);
185        fire_expire_hooks(ctx);
186
187        return;
188    }
189
190    schedule_ready_after_cast(ctx, &spell, now);
191
192    let shares_base_cooldown = overridden && requested_spell.overrides.shares_base_cooldown;
193
194    if spell.cooldown.has_cooldown && !cooldown_bypassed {
195        let cooldown_local = if shares_base_cooldown {
196            ctx.state
197                .spell_local(requested_spell.spell_id)
198                .unwrap_or(spell_local)
199        } else {
200            spell_local
201        };
202        let cooldown_ms =
203            start_cooldown_in_slot(ctx.state, ctx.buf, spell_local, cooldown_local, now);
204
205        ctx.sink
206            .emit_cooldown_start(spell.spell_id, cooldown_ms, now.as_millis());
207    }
208
209    let adjustment = empower_release_adjustment(ctx, spell_local, empower_rank);
210    let payload = adjust_empower_payload(
211        ctx.state,
212        &spell,
213        finisher_scaled_damage(ctx, &spell),
214        adjustment,
215    );
216
217    process_cast_impact(ctx, &spell, payload);
218
219    process_cdr_effects(ctx, &spell);
220
221    fire_cast_hook(ctx, spell_local, empower_rank);
222
223    fire_player_cast_hooks(ctx, spell.spell_id, empower_rank);
224
225    consume_cast_auras(
226        ctx,
227        override_aura_to_consume,
228        cooldown_aura_to_consume,
229        spell.requirements.consume_aura_on_cast_id,
230    );
231
232    if spell.behavior.breaks_stealth {
233        break_stealth_if_active(&mut ctx.hook_ctx());
234    }
235
236    if let Some(s) = ctx.buf.spell_mut(SpellIdx(spell.spell_id)) {
237        s.last_used = now.as_secs_f64();
238    }
239
240    update_history(ctx.buf, SpellIdx(spell.spell_id), spell.behavior.off_gcd);
241
242    fire_spell_phase_procs(ctx, spell.spell_id, crate::state::ProcPhaseMask::FINISH);
243    fire_expire_hooks(ctx);
244}