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  pub(crate) fn kind_mut(&mut self) -> &mut ManeuverKind {
155    &mut self.kind
156  }
157
158  #[inline]
159  pub fn direction(&self) -> ManeuverDirection {
160    self.direction
161  }
162
163  #[inline]
164  pub fn state(&self) -> &ManeuverState {
165    &self.state
166  }
167
168  #[inline]
169  pub fn speed(&self) -> Speed {
170    self.speed
171  }
172
173  #[inline]
174  pub fn hauled_resources(&self) -> Option<&ManeuverHaul> {
175    self.hauled_resources.as_ref()
176  }
177
178  #[inline]
179  pub(crate) fn hauled_resources_mut(&mut self) -> &mut Option<ManeuverHaul> {
180    &mut self.hauled_resources
181  }
182
183  #[inline]
184  pub fn is_attack(&self) -> bool {
185    self.kind.is_attack()
186  }
187
188  #[inline]
189  pub fn is_support(&self) -> bool {
190    self.kind.is_support()
191  }
192
193  #[inline]
194  pub fn is_done(&self) -> bool {
195    self.state.is_done()
196  }
197
198  #[inline]
199  pub fn is_pending(&self) -> bool {
200    self.state.is_pending()
201  }
202
203  #[inline]
204  pub fn is_going(&self) -> bool {
205    self.direction.is_going()
206  }
207
208  #[inline]
209  pub fn is_returning(&self) -> bool {
210    self.direction.is_returning()
211  }
212
213  /// Checks whether the maneuver's origin or destination matches the coord.
214  #[inline]
215  pub fn matches_coord(&self, coord: Coord) -> bool {
216    coord == self.origin || coord == self.destination
217  }
218
219  #[inline]
220  pub fn distance(&self) -> Distance {
221    self.origin.distance(self.destination)
222  }
223
224  pub fn pending_distance(&self) -> Option<ManeuverDistance> {
225    if let ManeuverState::Pending { distance } = self.state {
226      Some(distance)
227    } else {
228      None
229    }
230  }
231}
232
233#[must_use]
234#[derive(
235  Clone,
236  Copy,
237  Debug,
238  derive_more::Display,
239  PartialEq,
240  Eq,
241  PartialOrd,
242  Ord,
243  Hash,
244  Deserialize,
245  Serialize,
246)]
247#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
248pub struct ManeuverId(Uuid);
249
250impl ManeuverId {
251  pub fn new() -> Self {
252    Self(Uuid::now_v7())
253  }
254}
255
256impl Default for ManeuverId {
257  fn default() -> Self {
258    Self::new()
259  }
260}
261
262#[derive(Copy, Debug, strum::Display, Deserialize, Serialize, EnumIs)]
263#[derive_const(Clone, PartialEq, Eq)]
264#[serde(rename_all = "kebab-case")]
265#[strum(serialize_all = "kebab-case")]
266#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
267pub enum ManeuverKind {
268  Attack,
269  Support,
270}
271
272#[derive(Copy, Debug, strum::Display, Deserialize, Serialize, EnumIs)]
273#[derive_const(Clone, Default, PartialEq, Eq)]
274#[serde(rename_all = "kebab-case")]
275#[strum(serialize_all = "kebab-case")]
276#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
277pub enum ManeuverDirection {
278  #[default]
279  Going,
280  Returning,
281}
282
283#[derive(Copy, Debug, strum::Display, Deserialize, Serialize, EnumIs)]
284#[derive_const(Clone, PartialEq, Eq)]
285#[serde(tag = "kind", rename_all = "kebab-case")]
286#[strum(serialize_all = "kebab-case")]
287#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
288pub enum ManeuverState {
289  Done,
290  Pending { distance: ManeuverDistance },
291}
292
293impl ManeuverState {
294  #[inline]
295  pub fn with_distance(distance: ManeuverDistance) -> Self {
296    Self::Pending { distance }
297  }
298
299  pub fn distance(&self) -> Option<ManeuverDistance> {
300    match self {
301      Self::Done => None,
302      Self::Pending { distance } => Some(*distance),
303    }
304  }
305}
306
307#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
308#[serde(rename_all = "camelCase")]
309#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
310pub struct ManeuverHaul {
311  ruler: Ruler,
312  resources: Resources,
313}
314
315impl ManeuverHaul {
316  #[inline]
317  pub fn ruler(&self) -> &Ruler {
318    &self.ruler
319  }
320
321  #[inline]
322  pub fn resources(&self) -> &Resources {
323    &self.resources
324  }
325}
326
327impl From<ManeuverHaul> for Resources {
328  fn from(haul: ManeuverHaul) -> Self {
329    haul.resources
330  }
331}
332
333#[derive(Builder, Clone, Debug, Deserialize, Serialize)]
334#[serde(rename_all = "camelCase")]
335#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
336pub struct ManeuverRequest {
337  pub kind: ManeuverKind,
338  #[builder(into)]
339  pub ruler: Ruler,
340  #[builder(into)]
341  pub origin: Coord,
342  #[builder(into)]
343  pub destination: Coord,
344  #[builder(into)]
345  pub personnel: ArmyPersonnel,
346}