wowlab_engine_combat/state/
control.rs1use 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#[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#[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 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;