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