Skip to main content

wowlab_engine_combat/handler/spec_handler/
combat_events.rs

1use super::{
2    ActorId, CombatCtxRequest, CombatHandler, DamageSchool, EnemyIdx, Event, GuardianHandle,
3    SimContext, SimTime, earliest_auto_attack_time, phase_auto_attacks_for, process_pet_action,
4};
5
6impl CombatHandler {
7    pub(super) fn on_guardian_action_impl(
8        &mut self,
9        guardian: wowlab_engine_ports::GuardianEventBinding,
10        ctx: &mut SimContext,
11    ) {
12        let Some((guardian_id, guardian_generation, Some(ability_index))) =
13            self.program.runtime_events.validate_guardian(guardian)
14        else {
15            return;
16        };
17        let now = ctx.state.current_time;
18        let handle = GuardianHandle::new(guardian_id, guardian_generation);
19        let Some((source, target)) =
20            crate::systems::guardian_combat_coordinates(&self.instance, handle)
21        else {
22            return;
23        };
24
25        self.with_combat_ctx(
26            &mut *ctx.telemetry,
27            CombatCtxRequest::new(now, source, target),
28            |combat| {
29                crate::systems::process_guardian_action(combat, handle, usize::from(ability_index));
30            },
31        );
32    }
33
34    pub(super) fn on_guardian_expire_impl(
35        &mut self,
36        guardian: wowlab_engine_ports::GuardianEventBinding,
37        ctx: &mut SimContext,
38    ) {
39        let Some((guardian_id, guardian_generation, None)) =
40            self.program.runtime_events.validate_guardian(guardian)
41        else {
42            return;
43        };
44        let now = ctx.state.current_time;
45        let handle = GuardianHandle::new(guardian_id, guardian_generation);
46        let Some((source, target)) =
47            crate::systems::guardian_combat_coordinates(&self.instance, handle)
48        else {
49            return;
50        };
51
52        self.with_combat_ctx(
53            &mut *ctx.telemetry,
54            CombatCtxRequest::new(now, source, target),
55            |combat| crate::systems::process_guardian_expire(combat, handle),
56        );
57    }
58
59    pub(super) fn on_pet_action_impl(
60        &mut self,
61        action: wowlab_engine_ports::PetActionBinding,
62        ctx: &mut SimContext,
63    ) {
64        let Some(auto_attack_index) = self
65            .program
66            .runtime_events
67            .validate_pet_action(action)
68            .and_then(|plan| {
69                self.program
70                    .pet_action_plans
71                    .get(usize::from(plan))
72                    .copied()
73            })
74            .map(usize::from)
75        else {
76            return;
77        };
78        let now = ctx.state.current_time;
79
80        let Some(target) = self.instance.current_target().or_else(|| {
81            self.instance.retarget(&self.program, now);
82
83            self.instance.current_target()
84        }) else {
85            return;
86        };
87
88        self.with_combat_ctx(
89            &mut *ctx.telemetry,
90            CombatCtxRequest::new(
91                now,
92                ActorId::Pet(wowlab_types::sim::PetIdx::PRIMARY),
93                target,
94            ),
95            |combat| process_pet_action(combat, auto_attack_index),
96        );
97    }
98
99    pub(super) fn on_proc_heartbeat_impl(&mut self, ctx: &mut SimContext) {
100        let now = ctx.state.current_time;
101
102        if !crate::systems::begin_proc_heartbeat(&self.program, &mut self.instance, now) {
103            return;
104        }
105
106        crate::systems::refresh_pet_stat_snapshots(&self.program, &mut self.instance);
107
108        if let Some(target) = self.instance.current_target() {
109            self.with_combat_ctx(
110                &mut *ctx.telemetry,
111                CombatCtxRequest::new(now, ActorId::Player, target),
112                crate::systems::fire_heartbeat_procs,
113            );
114        }
115
116        crate::systems::ensure_recurring_proc_heartbeat(&mut self.instance, now, &mut self.rng);
117    }
118
119    pub(super) fn on_auto_attack_impl(
120        &mut self,
121        source: ActorId,
122        target: EnemyIdx,
123        ctx: &mut SimContext,
124    ) {
125        let now = ctx.state.current_time;
126
127        if !self.instance.is_valid_target(target) {
128            self.instance.retarget(&self.program, now);
129
130            if let Some(target) = self.instance.current_target() {
131                self.instance.schedule(Event::AutoAttack {
132                    t: now,
133                    source,
134                    target,
135                });
136            }
137
138            return;
139        }
140
141        let swing_was_due =
142            earliest_auto_attack_time(&self.program, &self.instance).is_some_and(|t| t <= now);
143
144        self.with_combat_ctx(
145            &mut *ctx.telemetry,
146            CombatCtxRequest::new(now, source, target),
147            phase_auto_attacks_for,
148        );
149
150        if swing_was_due
151            && let Some(next_t) = earliest_auto_attack_time(&self.program, &self.instance)
152            && next_t > now
153            && let Some(target) = self.instance.current_target()
154        {
155            self.instance.schedule(Event::AutoAttack {
156                t: next_t,
157                source,
158                target,
159            });
160        }
161    }
162
163    pub(super) fn on_enemy_auto_attack_impl(
164        &mut self,
165        source: EnemyIdx,
166        target: ActorId,
167        ctx: &mut SimContext,
168    ) {
169        if !self
170            .instance
171            .enemy_auto_attack_is_current(source, ctx.state.current_time)
172            || !self.instance.is_valid_target(source)
173            || !self.instance.is_friendly_actor_alive(target)
174        {
175            return;
176        }
177
178        let Some((damage, swing_ms)) = self
179            .program
180            .enemy_definition(source)
181            .and_then(|enemy| Some((enemy.auto_attack_damage()?, enemy.auto_attack_swing_ms()?)))
182        else {
183            return;
184        };
185
186        let _ = crate::systems::deal_incoming_damage(
187            &self.program,
188            &mut self.instance,
189            crate::systems::IncomingDamage {
190                source: ActorId::Enemy(source),
191                target,
192                amount: damage,
193                school: DamageSchool::Physical,
194                mechanic_mask: 0,
195                is_aoe: false,
196                is_periodic: false,
197                at: ctx.state.current_time,
198            },
199        );
200
201        if self.instance.is_valid_target(source) && self.instance.is_friendly_actor_alive(target) {
202            self.instance.schedule_enemy_auto_attack(
203                source,
204                target,
205                ctx.state
206                    .current_time
207                    .saturating_add(SimTime::from_millis(swing_ms)),
208            );
209        }
210    }
211}