Skip to main content

wowlab_engine_combat/systems/
health_thresholds.rs

1use 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;