nil_core/military/maneuver/
mod.rs1pub 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 #[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}