wowlab_engine_combat/state/combat_state/
enemies.rs1use super::{
2 ActorId, AttackPosition, BecameDead, CombatState, EnemyActorDefinition, EnemyDespawnOutcome,
3 EnemyIdx, EnemyState, Event, HealthModel, PullId, SimTime, SpatialActor, SpatialQueryHandle,
4 SpecRuntimeError, parry_hasted_deadline,
5};
6
7impl CombatState {
8 #[must_use]
9 pub fn enemy_definition(&self, enemy: EnemyIdx) -> Option<&EnemyActorDefinition> {
10 self.defs
11 .enemies
12 .get(enemy.as_usize())
13 .filter(|definition| definition.id() == enemy)
14 }
15
16 #[must_use]
17 pub fn enemy(&self, enemy: EnemyIdx) -> Option<&EnemyState> {
18 self.runtime
19 .enemies
20 .get(enemy.as_usize())
21 .filter(|state| state.id() == enemy)
22 }
23
24 #[must_use]
26 pub fn enemy_max_health(&self, enemy: EnemyIdx) -> Option<f64> {
27 self.enemy_definition(enemy)
28 .map(|definition| definition.health_model().max_health())
29 }
30
31 #[must_use]
33 pub fn enemy_health(&self, enemy: EnemyIdx, now: SimTime) -> Option<f64> {
34 let definition = self.enemy_definition(enemy)?;
35 let state = self.enemy(enemy)?;
36
37 match definition.health_model() {
38 HealthModel::DamageDriven { .. } => Some(state.current_health()),
39 health_model @ HealthModel::ScriptedLinear { .. } => {
40 health_model.scripted_health_at(state.spawned_at()?, now)
41 }
42 }
43 }
44
45 #[must_use]
47 pub fn enemy_health_fraction(&self, enemy: EnemyIdx, now: SimTime) -> Option<f64> {
48 let max_health = self.enemy_max_health(enemy)?;
49
50 (max_health > 0.0).then(|| {
51 self.enemy_health(enemy, now)
52 .map(|health| health / max_health)
53 })?
54 }
55
56 #[must_use]
58 pub fn actor_health_fraction(&self, actor: ActorId, now: SimTime) -> Option<f64> {
59 match actor {
60 ActorId::External => None,
61 ActorId::Player => Some(self.runtime.health.player_health.fraction()),
62 ActorId::Pet(pet) => self
63 .runtime
64 .health
65 .pet_health
66 .get(pet.as_usize())
67 .and_then(Option::as_ref)
68 .map(|health| health.fraction()),
69 ActorId::Enemy(enemy) => self.enemy_health_fraction(enemy, now),
70 }
71 }
72
73 #[must_use]
74 pub fn enemy_definitions(&self) -> impl ExactSizeIterator<Item = &EnemyActorDefinition> {
75 self.defs.enemies.iter()
76 }
77
78 #[must_use]
79 pub fn enemies(&self) -> impl ExactSizeIterator<Item = &EnemyState> {
80 self.runtime.enemies.iter()
81 }
82
83 #[must_use]
89 pub fn active_enemy_count(&self) -> i32 {
90 let count = self.enemies().filter(|enemy| enemy.is_active()).count();
91
92 i32::try_from(count).expect("validated encounter enemy count fits i32")
93 }
94
95 #[must_use]
101 pub fn active_alive_enemy_count(&self) -> i32 {
102 let count = self
103 .enemies()
104 .filter(|enemy| enemy.is_active() && enemy.is_alive())
105 .count();
106
107 i32::try_from(count).expect("validated encounter enemy count fits i32")
108 }
109
110 pub fn reset_enemies(&mut self) {
116 self.runtime.enemies = self
117 .defs
118 .enemies
119 .iter()
120 .map(EnemyState::from_definition)
121 .collect();
122 self.runtime.player_transform = self.config.encounter.definition().initial_player_transform;
123 self.runtime.attack_position = AttackPosition::Behind;
124 self.runtime.health.enemy_swing_deadlines.fill(None);
125 self.runtime
126 .health
127 .enemy_invulnerable_until
128 .fill(SimTime::ZERO);
129 self.runtime
130 .encounter
131 .reset(self.config.encounter.definition());
132 let active: Vec<_> = self
133 .runtime
134 .enemies
135 .iter()
136 .filter(|enemy| enemy.is_active() && enemy.is_alive())
137 .map(|enemy| SpatialActor {
138 id: enemy.id(),
139 transform: enemy.current_transform(),
140 })
141 .collect();
142
143 self.spatial = SpatialQueryHandle(Box::new(
144 wowlab_engine_spatial::build_spatial_query(
145 &self.config.encounter.definition().spatial_scene,
146 &active,
147 )
148 .expect("validated encounter spatial scene rebuilds deterministically"),
149 ));
150 self.retarget();
151 }
152
153 #[must_use]
154 pub fn is_valid_target(&self, enemy: EnemyIdx) -> bool {
155 self.enemy(enemy)
156 .is_some_and(|actor| actor.is_active() && actor.is_alive())
157 }
158
159 #[must_use]
160 pub const fn current_target(&self) -> Option<EnemyIdx> {
161 self.runtime.current_target
162 }
163
164 #[must_use]
165 pub const fn attack_position(&self) -> AttackPosition {
166 self.runtime.attack_position
167 }
168
169 pub const fn set_attack_position(&mut self, position: AttackPosition) {
170 self.runtime.attack_position = position;
171 }
172
173 pub fn retarget(&mut self) -> Option<EnemyIdx> {
175 let active_pull = self.runtime.encounter.active_pull();
176 let pull = self
177 .config
178 .encounter
179 .definition()
180 .pulls
181 .get(active_pull.as_usize())
182 .filter(|pull| pull.id == active_pull);
183 let preferred = pull.map(|pull| pull.preferred_target);
184
185 self.runtime.current_target = preferred
186 .filter(|enemy| self.is_valid_target(*enemy))
187 .or_else(|| {
188 self.enemies()
189 .find(|enemy| {
190 enemy.is_active()
191 && enemy.is_alive()
192 && self.enemy_belongs_to_pull(enemy.id(), active_pull)
193 })
194 .map(EnemyState::id)
195 });
196
197 self.runtime.current_target
198 }
199
200 pub fn activate_enemy(&mut self, enemy: EnemyIdx, at: SimTime) -> bool {
201 let Some(actor) = self.enemy(enemy) else {
202 return false;
203 };
204
205 if actor.is_active() || !actor.is_alive() {
206 return false;
207 }
208
209 let transform = actor.current_transform();
210 let spatial_actor = SpatialActor {
211 id: enemy,
212 transform,
213 };
214
215 if self.spatial.insert_actor(spatial_actor).is_err() {
216 return false;
217 }
218
219 if !self
220 .enemy_mut(enemy)
221 .is_some_and(|actor| actor.activate(at))
222 {
223 let _ = self.spatial.remove_actor(spatial_actor);
224
225 return false;
226 }
227
228 true
229 }
230
231 pub fn refresh_enemy_health(&mut self, enemy: EnemyIdx, now: SimTime) {
232 if let Some(state) = self
233 .enemy_mut(enemy)
234 .filter(|state| state.is_active() && state.is_alive())
235 {
236 state.refresh_scripted_health(now);
237 }
238 }
239
240 #[must_use]
241 pub fn enemy_time_to_die(&self, enemy: EnemyIdx, now: SimTime) -> Option<f64> {
242 self.enemy(enemy)?.time_to_die(now)
243 }
244
245 pub fn apply_enemy_damage(
251 &mut self,
252 enemy: EnemyIdx,
253 amount: f64,
254 at: SimTime,
255 ) -> Result<Option<BecameDead>, SpecRuntimeError> {
256 let state = self
257 .runtime
258 .enemies
259 .get_mut(enemy.as_usize())
260 .filter(|state| state.id() == enemy)
261 .ok_or_else(|| SpecRuntimeError::missing_enemy(enemy))?;
262 let transform = state.current_transform();
263 let remove_from_index = state.is_active();
264 let mut next_state = state.clone();
265 let outcome = next_state.apply_damage(amount, at);
266
267 if outcome.is_some() && remove_from_index {
268 self.spatial
269 .remove_actor(SpatialActor {
270 id: enemy,
271 transform,
272 })
273 .map_err(|source| SpecRuntimeError::enemy_death_spatial_removal(enemy, source))?;
274 }
275
276 *state = next_state;
277
278 Ok(outcome)
279 }
280
281 pub(crate) fn schedule_enemy_auto_attack(
282 &mut self,
283 source: EnemyIdx,
284 target: ActorId,
285 at: SimTime,
286 ) {
287 let Some(deadline) = self
288 .runtime
289 .health
290 .enemy_swing_deadlines
291 .get_mut(source.as_usize())
292 else {
293 return;
294 };
295
296 *deadline = Some(at);
297 self.schedule(Event::EnemyAutoAttack {
298 t: at,
299 source,
300 target,
301 });
302 }
303
304 pub(crate) fn grant_enemy_invulnerability(&mut self, target: EnemyIdx, until: SimTime) {
305 if let Some(existing) = self
306 .runtime
307 .health
308 .enemy_invulnerable_until
309 .get_mut(target.as_usize())
310 {
311 *existing = (*existing).max(until);
312 }
313 }
314
315 #[must_use]
316 pub(crate) fn enemy_is_invulnerable(&self, target: EnemyIdx, now: SimTime) -> bool {
317 self.runtime
318 .health
319 .enemy_invulnerable_until
320 .get(target.as_usize())
321 .is_some_and(|until| now < *until)
322 }
323
324 pub(crate) fn enemy_auto_attack_is_current(&self, source: EnemyIdx, at: SimTime) -> bool {
325 self.runtime
326 .health
327 .enemy_swing_deadlines
328 .get(source.as_usize())
329 .is_some_and(|deadline| *deadline == Some(at))
330 }
331
332 pub(crate) fn apply_enemy_parry_haste(&mut self, source: EnemyIdx, now: SimTime) {
333 let Some(base_ms) = self
334 .enemy_definition(source)
335 .and_then(EnemyActorDefinition::auto_attack_swing_ms)
336 else {
337 return;
338 };
339 let Some(Some(current)) = self
340 .runtime
341 .health
342 .enemy_swing_deadlines
343 .get(source.as_usize())
344 else {
345 return;
346 };
347 let Some(accelerated_deadline) = parry_hasted_deadline(base_ms, *current, now) else {
348 return;
349 };
350
351 self.schedule_enemy_auto_attack(source, ActorId::Player, accelerated_deadline);
352 }
353
354 #[must_use]
355 pub(crate) fn enemy_belongs_to_pull(&self, enemy: EnemyIdx, pull: PullId) -> bool {
356 let definition = self.config.encounter.definition();
357 let Some(actor) = definition.enemies.get(enemy.as_usize()) else {
358 return false;
359 };
360 let Some(group) = definition.groups.get(actor.group_id.as_usize()) else {
361 return false;
362 };
363
364 definition
365 .waves
366 .get(group.wave_id.as_usize())
367 .is_some_and(|wave| wave.pull_id == pull)
368 }
369
370 pub(crate) fn transition_enemy_despawn(
371 &mut self,
372 enemy: EnemyIdx,
373 at: SimTime,
374 ) -> EnemyDespawnOutcome {
375 let Some(actor) = self.enemy(enemy) else {
376 return EnemyDespawnOutcome::UnknownEnemy;
377 };
378
379 if !actor.is_active() {
380 return EnemyDespawnOutcome::AlreadyInactive;
381 }
382
383 let transform = actor.current_transform();
384 let spatial_actor = SpatialActor {
385 id: enemy,
386 transform,
387 };
388
389 if self.spatial.remove_actor(spatial_actor).is_err() {
390 return EnemyDespawnOutcome::AlreadyInactive;
391 }
392
393 if !self.enemy_mut(enemy).is_some_and(|actor| actor.despawn(at)) {
394 let _ = self.spatial.insert_actor(spatial_actor);
395
396 return EnemyDespawnOutcome::AlreadyInactive;
397 }
398
399 EnemyDespawnOutcome::Despawned
400 }
401
402 pub(crate) fn finalize_scripted_enemy_death(
403 &mut self,
404 enemy: EnemyIdx,
405 at: SimTime,
406 ) -> Result<bool, SpecRuntimeError> {
407 let state = self
408 .runtime
409 .enemies
410 .get_mut(enemy.as_usize())
411 .filter(|state| state.id() == enemy)
412 .ok_or_else(|| SpecRuntimeError::missing_enemy(enemy))?;
413
414 if !state.is_active() || !state.is_alive() {
415 return Ok(false);
416 }
417
418 let transform = state.current_transform();
419 let mut next_state = state.clone();
420
421 if !next_state.finalize_scripted_death(at) {
422 return Ok(false);
423 }
424
425 self.spatial
426 .remove_actor(SpatialActor {
427 id: enemy,
428 transform,
429 })
430 .map_err(|source| SpecRuntimeError::enemy_death_spatial_removal(enemy, source))?;
431 *state = next_state;
432
433 Ok(true)
434 }
435
436 pub(super) fn enemy_mut(&mut self, enemy: EnemyIdx) -> Option<&mut EnemyState> {
437 self.runtime
438 .enemies
439 .get_mut(enemy.as_usize())
440 .filter(|state| state.id() == enemy)
441 }
442}