wowlab_engine_combat/systems/
health_thresholds.rs1use wowlab_engine_domain::rotation::DenseBuffer;
2use wowlab_engine_ports::Event;
3use wowlab_types::sim::{ActorId, SimTime, SpellIdx};
4
5use super::buffs::for_each_active_actor_aura_effect;
6use crate::state::{BuffEffect, CombatState, HealthThresholdDirection};
7
8const fn condition_met(direction: HealthThresholdDirection, health: f64, threshold: f64) -> bool {
9 match direction {
10 HealthThresholdDirection::Above => health > threshold,
11 HealthThresholdDirection::Below => health < threshold,
12 }
13}
14
15const fn crossed(
16 direction: HealthThresholdDirection,
17 before: f64,
18 after: f64,
19 threshold: f64,
20) -> bool {
21 match direction {
22 HealthThresholdDirection::Above => before <= threshold && after > threshold,
23 HealthThresholdDirection::Below => before >= threshold && after < threshold,
24 }
25}
26
27fn schedule_trigger(state: &mut CombatState, actor: ActorId, spell_id: u32, now: SimTime) {
28 let target = match actor {
29 ActorId::Enemy(enemy) => Some(enemy),
30 ActorId::Player | ActorId::External | ActorId::Pet(_) => state.current_target(),
31 };
32
33 if let Some(target) = target {
34 state.schedule(Event::CastComplete {
35 t: now,
36 spell_id: SpellIdx::from_raw(spell_id),
37 empower_rank: 0,
38 source: actor,
39 target,
40 });
41 }
42}
43
44pub(crate) fn process_health_threshold_application(
45 state: &mut CombatState,
46 actor: ActorId,
47 effects: &[Option<BuffEffect>],
48 now: SimTime,
49) {
50 let Some(health_pct) = state
51 .actor_health_fraction(actor, now)
52 .map(|fraction| fraction * wowlab_types::constants::HUNDRED)
53 else {
54 return;
55 };
56 let triggered: Vec<_> = effects
57 .iter()
58 .flatten()
59 .filter_map(|effect| match effect {
60 BuffEffect::HealthThresholdTrigger {
61 threshold_pct,
62 direction,
63 spell_id,
64 } if condition_met(*direction, health_pct, *threshold_pct) => Some(*spell_id),
65 _ => None,
66 })
67 .collect();
68
69 for spell_id in triggered {
70 schedule_trigger(state, actor, spell_id, now);
71 }
72}
73
74pub(crate) fn process_actor_health_change(
75 state: &mut CombatState,
76 buf: &DenseBuffer,
77 actor: ActorId,
78 before: f64,
79 after: f64,
80 now: SimTime,
81) {
82 let before_pct = before * wowlab_types::constants::HUNDRED;
83 let after_pct = after * wowlab_types::constants::HUNDRED;
84 let mut triggered = Vec::new();
85
86 for_each_active_actor_aura_effect(
87 crate::context::ActorView::new(state, buf, actor),
88 |_stacks, effect| {
89 let BuffEffect::HealthThresholdTrigger {
90 threshold_pct,
91 direction,
92 spell_id,
93 } = effect
94 else {
95 return;
96 };
97
98 if crossed(*direction, before_pct, after_pct, *threshold_pct) {
99 triggered.push(*spell_id);
100 }
101 },
102 );
103
104 for spell_id in triggered {
105 schedule_trigger(state, actor, spell_id, now);
106 }
107}
108
109#[cfg(test)]
110#[path = "health_thresholds/tests.rs"]
111mod tests;