wowlab_engine_combat/systems/
absorbs.rs1use 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 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 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
83pub(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 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 let index = state.runtime.scratch.absorb_indices[position];
128 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;