wowlab_engine_combat/handler/spec_handler/
combat_events.rs1use 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}