Skip to main content

wowlab_engine_combat/systems/
absorbs.rs

1//! Shared mutable absorb-pool consumption for damage and healing pipelines.
2
3use wowlab_engine_domain::dbc::SpellSchoolMask;
4use wowlab_types::sim::ActorId;
5
6use crate::{
7    state::{
8        AbsorbKind, CombatState, OverkillAbsorbDecision, OverkillAbsorbEvent,
9        OverkillAbsorbScriptContext,
10    },
11    systems::IncomingDamage,
12};
13
14const UNBOUNDED_OVERKILL_MAX_HEALTH_MULTIPLIER: f64 = 2.0;
15
16pub(crate) fn consume_absorbs(
17    state: &mut CombatState,
18    target: ActorId,
19    kind: AbsorbKind,
20    schools: SpellSchoolMask,
21    amount: f64,
22) -> f64 {
23    let mut indices: Vec<_> = state
24        .runtime
25        .health
26        .active_absorbs
27        .iter()
28        .enumerate()
29        .filter(|(_, absorb)| {
30            absorb.key.affected() == target
31                && absorb.kind == kind
32                && (absorb.schools.is_empty() || absorb.schools.intersects(schools))
33                && absorb.remaining > f64::EPSILON
34        })
35        .map(|(index, _)| index)
36        .collect();
37
38    indices.sort_by_key(|&index| {
39        // BOUNDS: indices were collected by enumerating this unchanged vector.
40        let absorb = state
41            .runtime
42            .health
43            .active_absorbs
44            .get(index)
45            .copied()
46            .expect("collected absorb index");
47
48        (
49            !absorb.high_priority,
50            absorb.priority,
51            absorb.application_order,
52        )
53    });
54
55    let mut remaining = amount.max(0.0);
56
57    for index in indices {
58        if remaining <= f64::EPSILON {
59            break;
60        }
61
62        // BOUNDS: indices were collected by enumerating this unchanged vector.
63
64        let absorb = state
65            .runtime
66            .health
67            .active_absorbs
68            .get_mut(index)
69            .expect("collected absorb index");
70        let consumed = remaining.min(absorb.remaining);
71
72        absorb.remaining -= consumed;
73        remaining -= consumed;
74    }
75
76    amount.max(0.0) - remaining
77}
78
79/// Resolves aura-316 scripts in absorb order without consuming their sentinel pools.
80pub(crate) fn resolve_overkill_absorbs(
81    state: &mut CombatState,
82    damage: &IncomingDamage,
83    amount: f64,
84    current_health: f64,
85    maximum_health: f64,
86) -> OverkillAbsorbDecision {
87    let schools = SpellSchoolMask::from(damage.school);
88    let mut candidates: Vec<_> = state
89        .runtime
90        .health
91        .active_absorbs
92        .iter()
93        .filter(|absorb| {
94            absorb.key.affected() == damage.target
95                && absorb.kind == AbsorbKind::Overkill
96                && absorb.schools.intersects(schools)
97        })
98        .copied()
99        .collect();
100
101    candidates.sort_by_key(|absorb| (absorb.priority, absorb.application_order));
102
103    let incoming_damage = amount.max(0.0);
104    let mut result = OverkillAbsorbDecision::default();
105    let mut remaining_damage = incoming_damage;
106
107    for absorb in candidates {
108        let within_threshold = if absorb.remaining > 0.0 {
109            incoming_damage <= absorb.remaining
110        } else {
111            incoming_damage < maximum_health * UNBOUNDED_OVERKILL_MAX_HEALTH_MULTIPLIER
112        };
113
114        if !within_threshold || remaining_damage <= f64::EPSILON {
115            continue;
116        }
117
118        let decision = if let Some(script) = absorb.overkill_script {
119            let mut context = OverkillAbsorbScriptContext::new(
120                state,
121                OverkillAbsorbEvent {
122                    aura: absorb.key,
123                    target: damage.target,
124                    school: schools,
125                    incoming_damage,
126                    remaining_damage,
127                    current_health,
128                    maximum_health,
129                    at: damage.at,
130                },
131            );
132
133            script(&mut context)
134        } else {
135            OverkillAbsorbDecision {
136                absorbed: remaining_damage,
137                death_fully_prevented: false,
138            }
139        };
140        let absorbed = decision.absorbed.max(0.0).min(remaining_damage);
141
142        if absorbed <= f64::EPSILON {
143            continue;
144        }
145
146        remaining_damage -= absorbed;
147        result.absorbed += absorbed;
148        result.death_fully_prevented |= decision.death_fully_prevented;
149    }
150
151    result
152}
153
154#[cfg(test)]
155mod tests;