Skip to main content

wowlab_engine_combat/state/
health.rs

1use wowlab_engine_domain::dbc::SpellSchoolMask;
2use wowlab_engine_ports::SpecRuntime;
3use wowlab_types::sim::{ActorId, AuraKey, SimTime};
4
5use super::CombatInstance;
6
7/// Pool selected by an absorb aura.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9#[non_exhaustive]
10pub enum AbsorbKind {
11    Damage,
12    Healing,
13    Overkill,
14}
15
16/// Aura-316 input resolved independently for each lethal hit.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct OverkillAbsorbEvent {
19    pub aura: AuraKey,
20    pub target: ActorId,
21    pub school: SpellSchoolMask,
22    pub incoming_damage: f64,
23    pub remaining_damage: f64,
24    pub current_health: f64,
25    pub maximum_health: f64,
26    pub at: SimTime,
27}
28
29/// Per-hit absorb amount and death-state decision returned by an aura-316 script.
30#[derive(Clone, Copy, Debug, Default, PartialEq)]
31pub struct OverkillAbsorbDecision {
32    pub absorbed: f64,
33    pub death_fully_prevented: bool,
34}
35
36/// Stateful runtime context exposed to an authored aura-316 script.
37#[derive(Debug)]
38#[must_use]
39pub struct OverkillAbsorbScriptContext<'a> {
40    instance: &'a mut CombatInstance,
41    event: OverkillAbsorbEvent,
42    pool: usize,
43}
44
45impl<'a> OverkillAbsorbScriptContext<'a> {
46    pub(crate) fn new(
47        instance: &'a mut CombatInstance,
48        event: OverkillAbsorbEvent,
49        pool: usize,
50    ) -> Self {
51        Self {
52            instance,
53            event,
54            pool,
55        }
56    }
57
58    /// Immutable per-hit inputs, including the active aura identity.
59    #[must_use]
60    pub const fn event(&self) -> OverkillAbsorbEvent {
61        self.event
62    }
63
64    /// Whether this aura application's script cooldown is ready for the current hit.
65    #[must_use]
66    pub fn cooldown_ready(&self) -> bool {
67        self.instance
68            .runtime
69            .pools
70            .overkill_absorb_ready_at
71            .is_ready(self.pool, self.event.at)
72    }
73
74    /// Starts this aura application's script cooldown from the current hit time.
75    pub fn start_cooldown(&mut self, duration: SimTime) -> SimTime {
76        self.instance
77            .runtime
78            .pools
79            .overkill_absorb_ready_at
80            .start_for(self.pool, self.event.at, duration)
81    }
82
83    /// Reads the current absolute health of a friendly combat actor.
84    #[must_use]
85    pub fn friendly_actor_health(&self, actor: ActorId) -> Option<f64> {
86        self.instance.friendly_actor_health(actor)
87    }
88
89    /// Downcasts the resettable per-spec runtime state.
90    #[must_use]
91    pub fn spec_runtime<T>(&self) -> Option<&T>
92    where
93        T: SpecRuntime,
94    {
95        self.instance.spec_runtime::<T>()
96    }
97
98    /// Mutably downcasts the resettable per-spec runtime state.
99    pub fn spec_runtime_mut<T>(&mut self) -> Option<&mut T>
100    where
101        T: SpecRuntime,
102    {
103        self.instance.spec_runtime_mut::<T>()
104    }
105}
106
107/// Authored aura-316 strategy invoked for every eligible lethal hit.
108pub(crate) type OverkillAbsorbScriptFn =
109    fn(&mut OverkillAbsorbScriptContext<'_>) -> OverkillAbsorbDecision;
110
111/// Static absorb payload contributed by an aura effect.
112#[derive(Clone, Copy, Debug)]
113pub struct AbsorbEffect {
114    pub amount: f64,
115    pub effect_index: u8,
116    pub schools: SpellSchoolMask,
117    pub kind: AbsorbKind,
118    pub high_priority: bool,
119    pub overkill_script: Option<OverkillAbsorbScriptFn>,
120    /// `TrinityCore`'s secondary absorb ordering key (`EffectMiscValue[1]`).
121    pub priority: i32,
122}
123
124/// Per-application mutable remainder for an absorb aura.
125#[derive(Clone, Copy, Debug)]
126pub(crate) struct ActiveAbsorb {
127    pub key: AuraKey,
128    pub pool: usize,
129    pub kind: AbsorbKind,
130    pub schools: SpellSchoolMask,
131    pub remaining: f64,
132    pub high_priority: bool,
133    pub overkill_script: Option<OverkillAbsorbScriptFn>,
134    pub priority: i32,
135    pub application_order: u64,
136}
137
138/// Orchestration-owned immunity interval consumed by the incoming-damage pipeline.
139#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140pub struct IncomingImmunityWindow {
141    pub actor: ActorId,
142    pub schools: SpellSchoolMask,
143    pub mechanic_mask: u64,
144    pub until: SimTime,
145}
146
147/// Mutable absolute-health state for a friendly combat actor.
148#[derive(Clone, Copy, Debug, PartialEq)]
149pub(crate) struct HealthPool {
150    current: f64,
151    maximum: f64,
152    fraction_when_unknown: f64,
153}
154
155impl HealthPool {
156    #[must_use]
157    pub(crate) fn full(maximum: f64) -> Self {
158        let maximum = maximum.max(0.0);
159
160        Self {
161            current: maximum,
162            maximum,
163            fraction_when_unknown: 1.0,
164        }
165    }
166
167    #[must_use]
168    pub(crate) const fn current(self) -> f64 {
169        self.current
170    }
171
172    #[must_use]
173    pub(crate) const fn maximum(self) -> f64 {
174        self.maximum
175    }
176
177    #[must_use]
178    pub(crate) fn fraction(self) -> f64 {
179        if self.maximum > 0.0 {
180            self.current / self.maximum
181        } else {
182            self.fraction_when_unknown
183        }
184    }
185
186    #[must_use]
187    pub(crate) fn is_alive(self) -> bool {
188        self.maximum <= 0.0 || self.current > 0.0
189    }
190
191    pub(crate) fn reset(&mut self) {
192        self.current = self.maximum;
193        self.fraction_when_unknown = 1.0;
194    }
195
196    pub(crate) fn set_maximum_preserve_fraction(&mut self, maximum: f64) {
197        let fraction = self.fraction();
198
199        self.maximum = maximum.max(0.0);
200        self.current = self.maximum * fraction;
201    }
202
203    pub(crate) fn set_maximum_clamp_current(&mut self, maximum: f64) {
204        self.maximum = maximum.max(0.0);
205        self.current = self.current.min(self.maximum);
206    }
207
208    pub(crate) fn set_fraction(&mut self, fraction: f64) -> f64 {
209        let previous = self.fraction();
210
211        let fraction = fraction.clamp(0.0, 1.0);
212
213        self.current = self.maximum * fraction;
214        self.fraction_when_unknown = fraction;
215
216        previous
217    }
218
219    /// Removes health and returns the amount that reached the pool after overkill clamping.
220    pub(crate) fn apply_damage(&mut self, amount: f64) -> f64 {
221        let amount = amount.max(0.0).min(self.current);
222
223        self.current -= amount;
224
225        amount
226    }
227
228    pub(crate) fn transition_to_dead(&mut self) {
229        self.current = 0.0;
230        self.fraction_when_unknown = 0.0;
231    }
232
233    /// Restores health and returns the effective amount after overheal clamping.
234    pub(crate) fn apply_heal(&mut self, amount: f64) -> f64 {
235        let amount = amount.max(0.0).min((self.maximum - self.current).max(0.0));
236
237        self.current += amount;
238
239        amount
240    }
241}
242
243#[cfg(test)]
244mod tests;