Skip to main content

wowlab_engine_combat/state/
control.rs

1use wowlab_types::sim::{ActorId, SimTime};
2
3use super::CombatInstance;
4#[cfg(feature = "rotation-optimizations")]
5use super::runtime::{RotationMutationDirection, RotationSemanticMutation};
6
7const DIMINISHING_RESET: SimTime = SimTime::from_millis(18_000);
8const HALF_DURATION: f64 = 0.5;
9const QUARTER_DURATION: f64 = 0.25;
10const IMMUNE_LEVEL: u8 = 3;
11
12pub(crate) use wowlab_engine_domain::dbc::{CrowdControlKind, DiminishingGroup};
13
14/// Outcome of applying crowd control after diminishing returns.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16#[non_exhaustive]
17pub enum CrowdControlApplication {
18    Applied {
19        duration: CrowdControlDuration,
20        level: u8,
21    },
22    Immune,
23}
24
25/// Duration of an active control effect.
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
27#[non_exhaustive]
28pub enum CrowdControlDuration {
29    Finite(SimTime),
30    Permanent,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub(crate) enum ControlDeadline {
35    At(SimTime),
36    Permanent,
37}
38
39impl ControlDeadline {
40    fn from_duration(duration: CrowdControlDuration, now: SimTime) -> Self {
41        match duration {
42            CrowdControlDuration::Finite(duration) => Self::At(now.saturating_add(duration)),
43            CrowdControlDuration::Permanent => Self::Permanent,
44        }
45    }
46
47    #[cfg(test)]
48    pub(crate) fn active(self, now: SimTime) -> bool {
49        match self {
50            Self::At(ends_at) => now < ends_at,
51            Self::Permanent => true,
52        }
53    }
54
55    pub(crate) fn later(self, other: Self) -> Self {
56        match (self, other) {
57            (Self::Permanent, _) | (_, Self::Permanent) => Self::Permanent,
58            (Self::At(left), Self::At(right)) => Self::At(left.max(right)),
59        }
60    }
61
62    fn reset_elapsed(self, now: SimTime) -> bool {
63        match self {
64            Self::At(ends_at) => now > ends_at.saturating_add(DIMINISHING_RESET),
65            Self::Permanent => false,
66        }
67    }
68}
69
70impl Default for ControlDeadline {
71    fn default() -> Self {
72        Self::At(SimTime::ZERO)
73    }
74}
75
76impl CrowdControlDuration {
77    fn adjusted(self, multiplier: f64) -> Self {
78        match self {
79            Self::Finite(duration) => {
80                Self::Finite(SimTime::from_secs_f64(duration.as_secs_f64() * multiplier))
81            }
82            Self::Permanent => Self::Permanent,
83        }
84    }
85}
86
87#[derive(Clone, Copy, Debug, Default)]
88pub(crate) struct DiminishingState {
89    pub(crate) level: u8,
90    pub(crate) last_control_ends: ControlDeadline,
91}
92
93impl CombatInstance {
94    /// Apply control to any combat actor with the `TrinityCore` 100/50/25/immune ladder.
95    pub fn apply_crowd_control(
96        &mut self,
97        actor: ActorId,
98        kind: CrowdControlKind,
99        group: DiminishingGroup,
100        duration: SimTime,
101        now: SimTime,
102    ) -> CrowdControlApplication {
103        self.apply_crowd_control_duration(
104            actor,
105            kind,
106            group,
107            CrowdControlDuration::Finite(duration),
108            now,
109        )
110    }
111
112    pub(crate) fn apply_crowd_control_duration(
113        &mut self,
114        actor: ActorId,
115        kind: CrowdControlKind,
116        group: DiminishingGroup,
117        duration: CrowdControlDuration,
118        now: SimTime,
119    ) -> CrowdControlApplication {
120        let (multiplier, level) = if group == DiminishingGroup::None {
121            (1.0, 1)
122        } else {
123            let dr = self
124                .runtime
125                .control
126                .diminishing_returns
127                .state_mut(actor, group);
128
129            if dr.last_control_ends.reset_elapsed(now) {
130                dr.level = 0;
131            }
132
133            let multiplier = match duration {
134                CrowdControlDuration::Permanent => 1.0,
135                CrowdControlDuration::Finite(_) => match dr.level {
136                    0 => 1.0,
137                    1 => HALF_DURATION,
138                    2 => QUARTER_DURATION,
139                    _ => return CrowdControlApplication::Immune,
140                },
141            };
142
143            dr.level = dr.level.saturating_add(1).min(IMMUNE_LEVEL);
144
145            (multiplier, dr.level)
146        };
147        let adjusted = duration.adjusted(multiplier);
148        let deadline = ControlDeadline::from_duration(adjusted, now);
149
150        if group != DiminishingGroup::None {
151            let dr = self
152                .runtime
153                .control
154                .diminishing_returns
155                .state_mut(actor, group);
156
157            dr.last_control_ends = dr.last_control_ends.later(deadline);
158        }
159
160        let key = (actor, kind);
161        let existing = self
162            .runtime
163            .control
164            .crowd_control
165            .get(key)
166            .copied()
167            .unwrap_or_default();
168
169        self.runtime
170            .control
171            .crowd_control
172            .insert(key, existing.later(deadline));
173
174        #[cfg(feature = "rotation-optimizations")]
175        if actor == ActorId::Player {
176            self.runtime.rotation_mutations.record_semantic(
177                RotationSemanticMutation::ActorState,
178                RotationMutationDirection::Fall,
179                now,
180            );
181        }
182
183        CrowdControlApplication::Applied {
184            duration: adjusted,
185            level,
186        }
187    }
188
189    #[must_use]
190    pub(crate) fn actor_cast_blocked_by_control(&self, actor: ActorId, now: SimTime) -> bool {
191        self.actor_control_ready_at(actor, now).is_some()
192            || self.actor_has_permanent_cast_control(actor)
193    }
194
195    #[must_use]
196    pub(crate) fn actor_control_ready_at(&self, actor: ActorId, now: SimTime) -> Option<SimTime> {
197        if self.actor_has_permanent_cast_control(actor) {
198            return None;
199        }
200
201        cast_blocking_control_kinds()
202            .filter_map(|kind| {
203                self.runtime
204                    .control
205                    .crowd_control
206                    .get((actor, kind))
207                    .copied()
208            })
209            .filter_map(|deadline| match deadline {
210                ControlDeadline::At(ends_at) if ends_at > now => Some(ends_at),
211                ControlDeadline::At(_) | ControlDeadline::Permanent => None,
212            })
213            .max()
214    }
215
216    fn actor_has_permanent_cast_control(&self, actor: ActorId) -> bool {
217        cast_blocking_control_kinds().any(|kind| {
218            self.runtime.control.crowd_control.get((actor, kind))
219                == Some(&ControlDeadline::Permanent)
220        })
221    }
222
223    #[must_use]
224    #[cfg(test)]
225    fn crowd_control_active(&self, actor: ActorId, kind: CrowdControlKind, now: SimTime) -> bool {
226        self.runtime
227            .control
228            .crowd_control
229            .get((actor, kind))
230            .is_some_and(|deadline| deadline.active(now))
231    }
232}
233
234fn cast_blocking_control_kinds() -> impl Iterator<Item = CrowdControlKind> {
235    [
236        CrowdControlKind::Stun,
237        CrowdControlKind::Fear,
238        CrowdControlKind::Silence,
239        CrowdControlKind::Incapacitate,
240    ]
241    .into_iter()
242}
243
244#[cfg(test)]
245mod tests;