Skip to main content

wowlab_engine_combat/state/
control.rs

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