wowlab_engine_combat/systems/
absorbs.rs1use 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 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 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
79pub(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;