wowlab_engine_combat/handler/spec_handler/
casting.rs1use super::{
2 ActorId, CombatCtxRequest, CombatHandler, EnemyIdx, Event, HookCtxRequest, LocalSpellIdx,
3 SimContext, SimTime, SpellIdx, check_recharge, drain_deferred_work, process_channel_tick_for,
4};
5
6#[derive(Clone, Copy)]
7enum SpellTravelEvent {
8 Launch,
9 Impact,
10}
11
12impl CombatHandler {
13 pub(super) fn on_cast_complete_impl(&mut self, event: Event, ctx: &mut SimContext) {
14 let Event::CastComplete {
15 spell_id,
16 empower_rank,
17 source,
18 target,
19 ..
20 } = event
21 else {
22 panic!("on_cast_complete received a non-cast-complete event");
23 };
24 let now = ctx.state.current_time;
25
26 if let Some(active) = self
27 .state
28 .runtime
29 .casting
30 .active_casts
31 .get(&source)
32 .copied()
33 {
34 if active.spell_id != spell_id || active.ends_at > now {
35 return;
36 }
37
38 self.state.runtime.casting.active_casts.remove(&source);
39 }
40
41 let is_off_gcd = self
42 .state
43 .spell_data(spell_id.as_u32())
44 .is_some_and(|(_, spell)| spell.behavior.off_gcd);
45
46 if is_off_gcd {
47 self.state.runtime.casting.instant_cast_pending_at = None;
48 }
49
50 if !self.state.is_valid_target(target) {
51 self.buf.player_mut().cast_end = 0.0;
52 self.state.schedule(Event::PlayerReady { t: now });
53
54 return;
55 }
56
57 self.with_combat_ctx(
58 &mut *ctx.telemetry,
59 CombatCtxRequest::new(now, source, target),
60 |combat| {
61 crate::pipeline::process_cast(combat, spell_id.as_u32(), empower_rank);
62
63 if let Some(binding) = combat
64 .state
65 .defs
66 .weapon_imbue_bindings
67 .iter()
68 .find(|binding| binding.spell_id == spell_id.as_u32())
69 .copied()
70 && let Some(slot) = combat.buf.weapon_imbue_mut(binding.key)
71 {
72 slot.is_active = 1;
73 slot.is_inactive = 0;
74 }
75 },
76 );
77 }
78
79 pub(super) fn on_triggered_spell_impl(
80 &mut self,
81 spell_id: SpellIdx,
82 source: ActorId,
83 target: ActorId,
84 ctx: &mut SimContext,
85 ) {
86 let now = ctx.state.current_time;
87 let anchor = match target {
88 ActorId::Enemy(enemy) => {
89 if !self.state.is_valid_target(enemy) {
90 return;
91 }
92
93 enemy
94 }
95 ActorId::Player | ActorId::Pet(_) | ActorId::External => {
96 self.state.current_target().unwrap_or(EnemyIdx::PRIMARY)
97 }
98 };
99
100 self.with_combat_ctx(
101 &mut *ctx.telemetry,
102 CombatCtxRequest::new(now, source, anchor),
103 |combat| {
104 crate::pipeline::process_triggered_spell(combat, spell_id.as_u32(), target);
105 },
106 );
107 self.with_hook_ctx(
108 &mut *ctx.telemetry,
109 HookCtxRequest::for_optional_target(now, Some(anchor))
110 .with_source(source)
111 .with_effect_target(target),
112 drain_deferred_work,
113 );
114 }
115
116 pub(super) fn on_spell_impact_impl(&mut self, impact_id: u32, ctx: &mut SimContext) {
117 self.process_spell_travel_event(impact_id, SpellTravelEvent::Impact, ctx);
118 }
119
120 pub(super) fn on_spell_launch_impl(&mut self, impact_id: u32, ctx: &mut SimContext) {
121 self.process_spell_travel_event(impact_id, SpellTravelEvent::Launch, ctx);
122 }
123
124 pub(super) fn on_channel_tick_impl(
125 &mut self,
126 spell_id: SpellIdx,
127 source: ActorId,
128 target: EnemyIdx,
129 generation: u64,
130 ctx: &mut SimContext,
131 ) {
132 let now = ctx.state.current_time;
133
134 self.with_combat_ctx(
135 &mut *ctx.telemetry,
136 CombatCtxRequest::new(now, source, target),
137 |combat| process_channel_tick_for(combat, spell_id.as_u32(), generation),
138 );
139 }
140
141 pub(super) fn on_actor_movement_impl(
142 &mut self,
143 actor: ActorId,
144 moving: bool,
145 generation: u64,
146 ctx: &mut SimContext,
147 ) {
148 crate::systems::apply_movement_event(
149 &mut self.state,
150 &mut self.buf,
151 actor,
152 moving,
153 generation,
154 ctx.state.current_time,
155 );
156 }
157
158 pub(super) fn on_cooldown_ready_impl(&mut self, _cooldown_key: SpellIdx, ctx: &mut SimContext) {
159 let now = ctx.state.current_time;
160
161 for i in 0..self.state.defs.spells.len() {
162 if self.state.defs.spells[i].is_charged() {
164 let local = u8::try_from(i).expect("spell count is validated below 256");
165
166 check_recharge(
167 &mut self.state,
168 &mut self.buf,
169 LocalSpellIdx::new(local),
170 now,
171 );
172 }
173 }
174 self.state.schedule(Event::PlayerReady { t: now });
177 }
178
179 pub(super) fn cast_time_ms_impl(&self, spell_id: SpellIdx, empower_rank: u8) -> u32 {
180 let Some((_, s)) = self.state.spell_data(spell_id.as_u32()) else {
181 return 0;
182 };
183
184 if s.channel.behavior.is_channel {
185 return 0;
186 }
187
188 if s.requirements.instant_cast_aura_id != 0
189 && crate::systems::aura_stacks_by_id(
190 &self.state,
191 &self.buf,
192 s.requirements.instant_cast_aura_id,
193 ActorId::Player,
194 self.state.current_target(),
195 ) > 0
196 {
197 return 0;
198 }
199
200 let cast_time_ms = if s.empower_rank_count > 0 {
201 let rank = empower_rank.max(1);
202 let index = usize::try_from(s.empower_cast_time_offset)
203 .ok()
204 .and_then(|offset| offset.checked_add(usize::from(rank.saturating_sub(1))));
205
206 index
207 .and_then(|index| self.state.defs.empower_cast_times_ms.get(index))
208 .copied()
209 .unwrap_or(0)
210 } else {
211 s.cast_time_ms
212 };
213
214 if cast_time_ms == 0 {
215 return 0;
216 }
217
218 crate::systems::effective_cast_time_ms(
219 &crate::context::CombatView::new(&self.state, &self.buf),
220 s.spell_id,
221 cast_time_ms,
222 )
223 }
224
225 pub(super) fn break_player_action_auras(
226 &mut self,
227 target: EnemyIdx,
228 now: SimTime,
229 telemetry: &mut wowlab_engine_telemetry::TelemetrySink,
230 ) {
231 self.with_hook_ctx(
232 telemetry,
233 HookCtxRequest::for_target(now, target).with_source(ActorId::Player),
234 |ctx| {
235 crate::systems::break_auras_on_actor(
236 ctx,
237 ActorId::Player,
238 crate::systems::AuraBreakTrigger::Action,
239 );
240 },
241 );
242 }
243
244 fn process_spell_travel_event(
245 &mut self,
246 impact_id: u32,
247 event: SpellTravelEvent,
248 ctx: &mut SimContext,
249 ) {
250 let now = ctx.state.current_time;
251 let Some((source, target)) = self
252 .state
253 .runtime
254 .pending
255 .pending_spell_impacts
256 .get(impact_id as usize)
257 .and_then(Option::as_ref)
258 .map(|impact| (impact.source, impact.target))
259 else {
260 return;
261 };
262
263 self.with_combat_ctx(
264 &mut *ctx.telemetry,
265 CombatCtxRequest::new(now, source, target),
266 |combat| match event {
267 SpellTravelEvent::Launch => {
268 crate::pipeline::process_spell_launch(combat, impact_id);
269 }
270 SpellTravelEvent::Impact => {
271 crate::pipeline::process_spell_impact(combat, impact_id);
272 }
273 },
274 );
275 }
276}