Skip to main content

nil_core/military/unit/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4pub mod r#impl;
5pub mod prelude;
6pub mod stats;
7
8use crate::error::Result;
9use crate::infrastructure::prelude::BuildingId;
10use crate::infrastructure::requirements::InfrastructureRequirements;
11use crate::ranking::score::Score;
12use crate::resources::prelude::*;
13use crate::world::config::WorldConfig;
14use derive_more::{Display, From, Into};
15use nil_util::ConstDeref;
16use serde::{Deserialize, Deserializer, Serialize, Serializer};
17use stats::prelude::*;
18use std::fmt;
19use std::num::NonZeroU32;
20use std::ops::Mul;
21use strum::EnumIter;
22use subenum::subenum;
23
24pub trait Unit: Send + Sync {
25  fn id(&self) -> UnitId;
26
27  fn kind(&self) -> UnitKind;
28
29  /// Building where the unit is recruited.
30  fn building(&self) -> BuildingId;
31
32  fn score(&self) -> Score;
33
34  fn stats(&self) -> &UnitStats;
35
36  fn power(&self) -> Power;
37
38  fn attack(&self) -> AttackPower;
39
40  fn defense(&self) -> DefensePower;
41
42  fn ranged_debuff(&self) -> RangedDebuff;
43
44  fn speed(&self, config: &WorldConfig) -> Speed;
45
46  fn haul(&self) -> Haul;
47
48  fn chunk(&self) -> &UnitChunk;
49
50  /// Building levels required to recruit the unit.
51  fn infrastructure_requirements(&self) -> &InfrastructureRequirements;
52
53  fn is_cavalry(&self) -> bool {
54    matches!(self.kind(), UnitKind::Cavalry)
55  }
56
57  fn is_infantry(&self) -> bool {
58    matches!(self.kind(), UnitKind::Infantry)
59  }
60
61  fn is_ranged(&self) -> bool {
62    matches!(self.kind(), UnitKind::Ranged)
63  }
64
65  fn is_offensive(&self) -> bool {
66    match self.id() {
67      UnitId::Archer | UnitId::Ram => true,
68      _ => {
69        let defense = self.defense();
70        *self.attack() > defense.infantry.max(defense.cavalry)
71      }
72    }
73  }
74
75  fn is_defensive(&self) -> bool {
76    match self.id() {
77      UnitId::Archer => true,
78      UnitId::Ram => false,
79      _ => !self.is_offensive(),
80    }
81  }
82
83  fn is_academy_unit(&self) -> bool {
84    matches!(self.building(), BuildingId::Academy)
85  }
86
87  fn is_stable_unit(&self) -> bool {
88    matches!(self.building(), BuildingId::Stable)
89  }
90
91  fn is_workshop_unit(&self) -> bool {
92    matches!(self.building(), BuildingId::Workshop)
93  }
94}
95
96#[subenum(AcademyUnitId, StableUnitId, WorkshopUnitId)]
97#[derive(Copy, Debug, Display, Hash, Deserialize, Serialize, EnumIter)]
98#[derive_const(Clone, PartialEq, Eq)]
99#[serde(rename_all = "kebab-case")]
100#[strum(serialize_all = "kebab-case")]
101#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
102#[cfg_attr(feature = "typescript", ts(export))]
103#[remain::sorted]
104pub enum UnitId {
105  #[subenum(AcademyUnitId)]
106  Archer,
107
108  #[subenum(AcademyUnitId)]
109  Axeman,
110
111  #[subenum(StableUnitId)]
112  HeavyCavalry,
113
114  #[subenum(StableUnitId)]
115  LightCavalry,
116
117  #[subenum(AcademyUnitId)]
118  Pikeman,
119
120  #[subenum(WorkshopUnitId)]
121  Ram,
122
123  #[subenum(AcademyUnitId)]
124  Swordsman,
125}
126
127impl From<UnitId> for BuildingId {
128  fn from(id: UnitId) -> Self {
129    UnitBox::from(id).0.building()
130  }
131}
132
133#[derive(derive_more::Deref)]
134#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
135#[cfg_attr(feature = "typescript", ts(as = "UnitId"))]
136pub struct UnitBox(Box<dyn Unit>);
137
138impl UnitBox {
139  #[inline]
140  pub fn new(unit: Box<dyn Unit>) -> Self {
141    Self(unit)
142  }
143
144  #[inline]
145  pub fn as_dyn(&self) -> &dyn Unit {
146    &*self.0
147  }
148}
149
150impl PartialEq for UnitBox {
151  fn eq(&self, other: &Self) -> bool {
152    self.0.id() == other.0.id()
153  }
154}
155
156impl Eq for UnitBox {}
157
158impl Clone for UnitBox {
159  fn clone(&self) -> Self {
160    Self::from(self.0.id())
161  }
162}
163
164impl fmt::Display for UnitBox {
165  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166    self.0.id().fmt(f)
167  }
168}
169
170impl fmt::Debug for UnitBox {
171  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172    f.debug_tuple("UnitBox")
173      .field(&self.0.id())
174      .finish()
175  }
176}
177
178impl From<UnitId> for UnitBox {
179  fn from(id: UnitId) -> Self {
180    use prelude::*;
181    match id {
182      UnitId::Archer => Archer::new_boxed(),
183      UnitId::Axeman => Axeman::new_boxed(),
184      UnitId::HeavyCavalry => HeavyCavalry::new_boxed(),
185      UnitId::LightCavalry => LightCavalry::new_boxed(),
186      UnitId::Pikeman => Pikeman::new_boxed(),
187      UnitId::Ram => Ram::new_boxed(),
188      UnitId::Swordsman => Swordsman::new_boxed(),
189    }
190  }
191}
192
193impl From<AcademyUnitId> for UnitBox {
194  fn from(id: AcademyUnitId) -> Self {
195    Self::from(UnitId::from(id))
196  }
197}
198
199impl From<StableUnitId> for UnitBox {
200  fn from(id: StableUnitId) -> Self {
201    Self::from(UnitId::from(id))
202  }
203}
204
205impl From<WorkshopUnitId> for UnitBox {
206  fn from(id: WorkshopUnitId) -> Self {
207    Self::from(UnitId::from(id))
208  }
209}
210
211impl Serialize for UnitBox {
212  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213  where
214    S: Serializer,
215  {
216    self.0.id().serialize(serializer)
217  }
218}
219
220impl<'de> Deserialize<'de> for UnitBox {
221  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
222  where
223    D: Deserializer<'de>,
224  {
225    Ok(Self::from(UnitId::deserialize(deserializer)?))
226  }
227}
228
229#[derive(Clone, Copy, Debug, strum::Display, PartialEq, Eq, Deserialize, Serialize)]
230#[serde(rename_all = "kebab-case")]
231#[strum(serialize_all = "kebab-case")]
232#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
233pub enum UnitKind {
234  Infantry,
235  Cavalry,
236  Ranged,
237}
238
239#[derive(Clone, Debug, Deserialize, Serialize)]
240#[serde(rename_all = "camelCase")]
241#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
242pub struct UnitChunk {
243  size: UnitChunkSize,
244  cost: Cost,
245  food_ratio: ResourceRatio,
246  iron_ratio: ResourceRatio,
247  stone_ratio: ResourceRatio,
248  wood_ratio: ResourceRatio,
249  maintenance_ratio: MaintenanceRatio,
250  workforce: Workforce,
251}
252
253impl UnitChunk {
254  #[inline]
255  pub fn size(&self) -> UnitChunkSize {
256    self.size
257  }
258
259  pub fn resources(&self) -> Resources {
260    Resources {
261      food: Food::from((self.cost * self.food_ratio).round()),
262      iron: Iron::from((self.cost * self.iron_ratio).round()),
263      stone: Stone::from((self.cost * self.stone_ratio).round()),
264      wood: Wood::from((self.cost * self.wood_ratio).round()),
265    }
266  }
267
268  #[inline]
269  pub fn maintenance(&self) -> Maintenance {
270    Maintenance::from((self.cost * self.maintenance_ratio).round())
271  }
272
273  #[inline]
274  pub fn workforce(&self) -> Workforce {
275    self.workforce
276  }
277}
278
279#[derive(Clone, Copy, Debug, Display, From, Into, Deserialize, Serialize, ConstDeref)]
280#[derive_const(Default, PartialEq, Eq, PartialOrd, Ord)]
281#[into(u8, u32)]
282#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
283pub struct UnitChunkSize(u8);
284
285impl UnitChunkSize {
286  #[inline]
287  pub const fn new(size: u8) -> UnitChunkSize {
288    UnitChunkSize(size)
289  }
290}
291
292impl const From<UnitChunkSize> for f64 {
293  fn from(value: UnitChunkSize) -> Self {
294    f64::from(value.0)
295  }
296}
297
298impl const Mul<u32> for UnitChunkSize {
299  type Output = u32;
300
301  fn mul(self, rhs: u32) -> Self::Output {
302    u32::from(self.0).saturating_mul(rhs)
303  }
304}
305
306impl const Mul<NonZeroU32> for UnitChunkSize {
307  type Output = u32;
308
309  fn mul(self, rhs: NonZeroU32) -> Self::Output {
310    self * rhs.get()
311  }
312}