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 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 #[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}