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