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}