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 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 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}