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, CombatInstance, 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 CombatInstance,
18    target: ActorId,
19    kind: AbsorbKind,
20    schools: SpellSchoolMask,
21    amount: f64,
22) -> f64 {
23    if state.runtime.health.active_absorbs.is_empty() {
24        return 0.0;
25    }
26
27    let count = {
28        let active = &state.runtime.health.active_absorbs;
29        let indices = &mut state.runtime.scratch.absorb_indices;
30
31        indices.clear();
32
33        for (index, absorb) in active.iter().enumerate() {
34            if absorb.key.affected() == target
35                && absorb.kind == kind
36                && (absorb.schools.is_empty() || absorb.schools.intersects(schools))
37                && absorb.remaining > f64::EPSILON
38            {
39                indices.push(index);
40            }
41        }
42
43        indices.sort_unstable_by_key(|&index| {
44            // BOUNDS: each index was collected by enumerating `active` above.
45            let absorb = active[index];
46
47            (
48                !absorb.high_priority,
49                absorb.priority,
50                absorb.application_order,
51            )
52        });
53
54        indices.len()
55    };
56
57    let mut remaining = amount.max(0.0);
58
59    for position in 0..count {
60        if remaining <= f64::EPSILON {
61            break;
62        }
63
64        // BOUNDS: `position` is bounded by the collected scratch length.
65
66        let index = state.runtime.scratch.absorb_indices[position];
67
68        let absorb = state
69            .runtime
70            .health
71            .active_absorbs
72            .get_mut(index)
73            .expect("collected absorb index");
74        let consumed = remaining.min(absorb.remaining);
75
76        absorb.remaining -= consumed;
77        remaining -= consumed;
78    }
79
80    amount.max(0.0) - remaining
81}
82
83/// Resolves aura-316 scripts in absorb order without consuming their sentinel pools.
84pub(crate) fn resolve_overkill_absorbs(
85    state: &mut CombatInstance,
86    damage: &IncomingDamage,
87    amount: f64,
88    current_health: f64,
89    maximum_health: f64,
90) -> OverkillAbsorbDecision {
91    if state.runtime.health.active_absorbs.is_empty() {
92        return OverkillAbsorbDecision::default();
93    }
94
95    let schools = SpellSchoolMask::from(damage.school);
96    let candidate_count = {
97        let active = &state.runtime.health.active_absorbs;
98        let indices = &mut state.runtime.scratch.absorb_indices;
99
100        indices.clear();
101
102        for (index, absorb) in active.iter().enumerate() {
103            if absorb.key.affected() == damage.target
104                && absorb.kind == AbsorbKind::Overkill
105                && absorb.schools.intersects(schools)
106            {
107                indices.push(index);
108            }
109        }
110
111        indices.sort_unstable_by_key(|&index| {
112            // BOUNDS: each index was collected by enumerating `active` above.
113            let absorb = active[index];
114
115            (absorb.priority, absorb.application_order)
116        });
117
118        indices.len()
119    };
120
121    let incoming_damage = amount.max(0.0);
122    let mut result = OverkillAbsorbDecision::default();
123    let mut remaining_damage = incoming_damage;
124
125    for position in 0..candidate_count {
126        // BOUNDS: `position` is bounded by the collected scratch length.
127        let index = state.runtime.scratch.absorb_indices[position];
128        // BOUNDS: each scratch index was collected by enumerating active absorbs.
129        let absorb = state.runtime.health.active_absorbs[index];
130        let within_threshold = if absorb.remaining > 0.0 {
131            incoming_damage <= absorb.remaining
132        } else {
133            incoming_damage < maximum_health * UNBOUNDED_OVERKILL_MAX_HEALTH_MULTIPLIER
134        };
135
136        if !within_threshold || remaining_damage <= f64::EPSILON {
137            continue;
138        }
139
140        let decision = if let Some(script) = absorb.overkill_script {
141            let mut context = OverkillAbsorbScriptContext::new(
142                state,
143                OverkillAbsorbEvent {
144                    aura: absorb.key,
145                    target: damage.target,
146                    school: schools,
147                    incoming_damage,
148                    remaining_damage,
149                    current_health,
150                    maximum_health,
151                    at: damage.at,
152                },
153                absorb.pool,
154            );
155
156            script(&mut context)
157        } else {
158            OverkillAbsorbDecision {
159                absorbed: remaining_damage,
160                death_fully_prevented: false,
161            }
162        };
163        let absorbed = decision.absorbed.max(0.0).min(remaining_damage);
164
165        if absorbed <= f64::EPSILON {
166            continue;
167        }
168
169        remaining_damage -= absorbed;
170        result.absorbed += absorbed;
171        result.death_fully_prevented |= decision.death_fully_prevented;
172    }
173
174    result
175}
176
177#[cfg(test)]
178mod tests;