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