Skip to main content

wowlab_engine_combat/handler/spec_handler/
casting.rs

1use 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            // #t(rust_unchecked_indexing) i bounded by spells.len()
163            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        // Cooldown readiness is a decision point; stale wakes are re-gated.
175
176        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}