Skip to main content

nil_core/military/maneuver/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4pub mod distance;
5
6#[cfg(test)]
7mod tests;
8
9use crate::continent::coord::Coord;
10use crate::continent::distance::Distance;
11use crate::error::{Error, Result};
12use crate::military::army::ArmyId;
13use crate::military::army::personnel::ArmyPersonnel;
14use crate::military::unit::stats::speed::Speed;
15use crate::resources::Resources;
16use crate::ruler::Ruler;
17use bon::Builder;
18use distance::ManeuverDistance;
19use serde::{Deserialize, Serialize};
20use strum::EnumIs;
21use uuid::Uuid;
22
23#[must_use]
24#[derive(Clone, Debug, Deserialize, Serialize)]
25#[serde(rename_all = "camelCase")]
26#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
27pub struct Maneuver {
28  id: ManeuverId,
29  origin: Coord,
30  destination: Coord,
31  army: ArmyId,
32  kind: ManeuverKind,
33  direction: ManeuverDirection,
34  state: ManeuverState,
35  speed: Speed,
36  hauled_resources: Option<ManeuverHaul>,
37}
38
39#[bon::bon]
40impl Maneuver {
41  #[builder]
42  pub(crate) fn new(
43    army: ArmyId,
44    kind: ManeuverKind,
45    origin: Coord,
46    destination: Coord,
47    speed: Speed,
48  ) -> Result<(ManeuverId, Self)> {
49    let distance = origin.distance(destination);
50    if origin == destination || distance == 0u8 {
51      return Err(Error::OriginIsDestination(origin));
52    }
53
54    let id = ManeuverId::new();
55    let state = ManeuverState::with_distance(distance.into());
56    let maneuver = Self {
57      id,
58      origin,
59      destination,
60      army,
61      kind,
62      direction: ManeuverDirection::Going,
63      state,
64      speed,
65      hauled_resources: None,
66    };
67
68    Ok((id, maneuver))
69  }
70
71  pub(super) fn advance(&mut self) -> Result<()> {
72    let is_done = match &mut self.state {
73      ManeuverState::Done => {
74        return Err(Error::ManeuverIsDone(self.id));
75      }
76      ManeuverState::Pending { distance } => {
77        debug_assert!(distance.is_finite());
78
79        *distance -= self.speed;
80        *distance = ((**distance).max(0.0)).into();
81        *distance <= 0.0
82      }
83    };
84
85    if is_done {
86      self.state = ManeuverState::Done;
87    }
88
89    Ok(())
90  }
91
92  pub(super) fn cancel(&mut self) -> Result<()> {
93    if self.is_returning() {
94      return Err(Error::ManeuverIsReturning(self.id));
95    }
96
97    let base_distance = self.distance();
98    match &mut self.state {
99      ManeuverState::Done => {
100        return Err(Error::ManeuverIsDone(self.id));
101      }
102      ManeuverState::Pending { distance } => {
103        debug_assert!(distance.is_finite());
104        debug_assert!(*distance > 0.0);
105
106        let covered = f64::from(base_distance - *distance).max(1.0);
107        self.state = ManeuverState::with_distance(covered.into());
108        self.direction = ManeuverDirection::Returning;
109      }
110    }
111
112    Ok(())
113  }
114
115  pub(crate) fn reverse(&mut self) -> Result<()> {
116    if self.is_pending() {
117      return Err(Error::ManeuverIsPending(self.id));
118    } else if self.is_returning() {
119      return Err(Error::ManeuverIsReturning(self.id));
120    }
121
122    let distance = self.distance();
123    self.state = ManeuverState::with_distance(distance.into());
124    self.direction = ManeuverDirection::Returning;
125
126    Ok(())
127  }
128
129  #[inline]
130  pub fn id(&self) -> ManeuverId {
131    self.id
132  }
133
134  #[inline]
135  pub fn origin(&self) -> Coord {
136    self.origin
137  }
138
139  #[inline]
140  pub fn destination(&self) -> Coord {
141    self.destination
142  }
143
144  #[inline]
145  pub fn army(&self) -> ArmyId {
146    self.army
147  }
148
149  #[inline]
150  pub fn kind(&self) -> ManeuverKind {
151    self.kind
152  }
153
154  #[inline]
155  pub fn direction(&self) -> ManeuverDirection {
156    self.direction
157  }
158
159  #[inline]
160  pub fn state(&self) -> &ManeuverState {
161    &self.state
162  }
163
164  #[inline]
165  pub fn speed(&self) -> Speed {
166    self.speed
167  }
168
169  #[inline]
170  pub fn hauled_resources(&self) -> Option<&ManeuverHaul> {
171    self.hauled_resources.as_ref()
172  }
173
174  #[inline]
175  pub(crate) fn hauled_resources_mut(&mut self) -> &mut Option<ManeuverHaul> {
176    &mut self.hauled_resources
177  }
178
179  #[inline]
180  pub fn is_attack(&self) -> bool {
181    self.kind.is_attack()
182  }
183
184  #[inline]
185  pub fn is_support(&self) -> bool {
186    self.kind.is_support()
187  }
188
189  #[inline]
190  pub fn is_done(&self) -> bool {
191    self.state.is_done()
192  }
193
194  #[inline]
195  pub fn is_pending(&self) -> bool {
196    self.state.is_pending()
197  }
198
199  #[inline]
200  pub fn is_going(&self) -> bool {
201    self.direction.is_going()
202  }
203
204  #[inline]
205  pub fn is_returning(&self) -> bool {
206    self.direction.is_returning()
207  }
208
209  /// Checks whether the maneuver's origin or destination matches the coord.
210  #[inline]
211  pub fn matches_coord(&self, coord: Coord) -> bool {
212    coord == self.origin || coord == self.destination
213  }
214
215  #[inline]
216  pub fn distance(&self) -> Distance {
217    self.origin.distance(self.destination)
218  }
219
220  pub fn pending_distance(&self) -> Option<ManeuverDistance> {
221    if let ManeuverState::Pending { distance } = self.state {
222      Some(distance)
223    } else {
224      None
225    }
226  }
227}
228
229#[must_use]
230#[derive(
231  Clone,
232  Copy,
233  Debug,
234  derive_more::Display,
235  PartialEq,
236  Eq,
237  PartialOrd,
238  Ord,
239  Hash,
240  Deserialize,
241  Serialize,
242)]
243#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
244pub struct ManeuverId(Uuid);
245
246impl ManeuverId {
247  pub fn new() -> Self {
248    Self(Uuid::now_v7())
249  }
250}
251
252impl Default for ManeuverId {
253  fn default() -> Self {
254    Self::new()
255  }
256}
257
258#[derive(Copy, Debug, strum::Display, Deserialize, Serialize, EnumIs)]
259#[derive_const(Clone, PartialEq, Eq)]
260#[serde(rename_all = "kebab-case")]
261#[strum(serialize_all = "kebab-case")]
262#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
263pub enum ManeuverKind {
264  Attack,
265  Support,
266}
267
268#[derive(Copy, Debug, strum::Display, Deserialize, Serialize, EnumIs)]
269#[derive_const(Clone, PartialEq, Eq)]
270#[serde(rename_all = "kebab-case")]
271#[strum(serialize_all = "kebab-case")]
272#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
273pub enum ManeuverDirection {
274  Going,
275  Returning,
276}
277
278#[derive(Copy, Debug, strum::Display, Deserialize, Serialize, EnumIs)]
279#[derive_const(Clone, PartialEq, Eq)]
280#[serde(tag = "kind", rename_all = "kebab-case")]
281#[strum(serialize_all = "kebab-case")]
282#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
283pub enum ManeuverState {
284  Done,
285  Pending { distance: ManeuverDistance },
286}
287
288impl ManeuverState {
289  #[inline]
290  pub fn with_distance(distance: ManeuverDistance) -> Self {
291    Self::Pending { distance }
292  }
293
294  pub fn distance(&self) -> Option<ManeuverDistance> {
295    match self {
296      Self::Done => None,
297      Self::Pending { distance } => Some(*distance),
298    }
299  }
300}
301
302#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
303#[serde(rename_all = "camelCase")]
304#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
305pub struct ManeuverHaul {
306  ruler: Ruler,
307  resources: Resources,
308}
309
310impl ManeuverHaul {
311  #[inline]
312  pub fn ruler(&self) -> &Ruler {
313    &self.ruler
314  }
315
316  #[inline]
317  pub fn resources(&self) -> &Resources {
318    &self.resources
319  }
320}
321
322impl From<ManeuverHaul> for Resources {
323  fn from(haul: ManeuverHaul) -> Self {
324    haul.resources
325  }
326}
327
328#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
329#[serde(rename_all = "camelCase")]
330#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
331pub struct ManeuverRequest {
332  pub kind: ManeuverKind,
333  #[builder(into)]
334  pub origin: Coord,
335  #[builder(into)]
336  pub destination: Coord,
337  #[builder(into)]
338  pub personnel: ArmyPersonnel,
339}