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(Clone, Copy, Debug, Display, Hash, Deserialize, Serialize, EnumIter)]
98#[derive_const(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::Debug for UnitBox {
165  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166    f.debug_tuple("UnitBox")
167      .field(&self.0.id())
168      .finish()
169  }
170}
171
172impl From<UnitId> for UnitBox {
173  fn from(id: UnitId) -> Self {
174    use prelude::*;
175    match id {
176      UnitId::Archer => Archer::new_boxed(),
177      UnitId::Axeman => Axeman::new_boxed(),
178      UnitId::HeavyCavalry => HeavyCavalry::new_boxed(),
179      UnitId::LightCavalry => LightCavalry::new_boxed(),
180      UnitId::Pikeman => Pikeman::new_boxed(),
181      UnitId::Ram => Ram::new_boxed(),
182      UnitId::Swordsman => Swordsman::new_boxed(),
183    }
184  }
185}
186
187impl From<AcademyUnitId> for UnitBox {
188  fn from(id: AcademyUnitId) -> Self {
189    Self::from(UnitId::from(id))
190  }
191}
192
193impl From<StableUnitId> for UnitBox {
194  fn from(id: StableUnitId) -> Self {
195    Self::from(UnitId::from(id))
196  }
197}
198
199impl From<WorkshopUnitId> for UnitBox {
200  fn from(id: WorkshopUnitId) -> Self {
201    Self::from(UnitId::from(id))
202  }
203}
204
205impl Serialize for UnitBox {
206  fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207  where
208    S: Serializer,
209  {
210    self.0.id().serialize(serializer)
211  }
212}
213
214impl<'de> Deserialize<'de> for UnitBox {
215  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216  where
217    D: Deserializer<'de>,
218  {
219    Ok(Self::from(UnitId::deserialize(deserializer)?))
220  }
221}
222
223#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
224#[serde(rename_all = "kebab-case")]
225#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
226pub enum UnitKind {
227  Infantry,
228  Cavalry,
229  Ranged,
230}
231
232#[derive(Clone, Debug, Deserialize, Serialize)]
233#[serde(rename_all = "camelCase")]
234#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
235pub struct UnitChunk {
236  size: UnitChunkSize,
237  cost: Cost,
238  food_ratio: ResourceRatio,
239  iron_ratio: ResourceRatio,
240  stone_ratio: ResourceRatio,
241  wood_ratio: ResourceRatio,
242  maintenance_ratio: MaintenanceRatio,
243  workforce: Workforce,
244}
245
246impl UnitChunk {
247  #[inline]
248  pub fn size(&self) -> UnitChunkSize {
249    self.size
250  }
251
252  pub fn resources(&self) -> Resources {
253    Resources {
254      food: Food::from((self.cost * self.food_ratio).round()),
255      iron: Iron::from((self.cost * self.iron_ratio).round()),
256      stone: Stone::from((self.cost * self.stone_ratio).round()),
257      wood: Wood::from((self.cost * self.wood_ratio).round()),
258    }
259  }
260
261  #[inline]
262  pub fn maintenance(&self) -> Maintenance {
263    Maintenance::from((self.cost * self.maintenance_ratio).round())
264  }
265
266  #[inline]
267  pub fn workforce(&self) -> Workforce {
268    self.workforce
269  }
270}
271
272#[derive(Clone, Copy, Debug, From, Into, Deserialize, Serialize, ConstDeref)]
273#[derive_const(Default, PartialEq, Eq, PartialOrd, Ord)]
274#[into(u8, u32)]
275#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
276pub struct UnitChunkSize(u8);
277
278impl UnitChunkSize {
279  #[inline]
280  pub const fn new(size: u8) -> UnitChunkSize {
281    UnitChunkSize(size)
282  }
283}
284
285impl const From<UnitChunkSize> for f64 {
286  fn from(value: UnitChunkSize) -> Self {
287    f64::from(value.0)
288  }
289}
290
291impl const Mul<u32> for UnitChunkSize {
292  type Output = u32;
293
294  fn mul(self, rhs: u32) -> Self::Output {
295    u32::from(self.0).saturating_mul(rhs)
296  }
297}
298
299impl const Mul<NonZeroU32> for UnitChunkSize {
300  type Output = u32;
301
302  fn mul(self, rhs: NonZeroU32) -> Self::Output {
303    self * rhs.get()
304  }
305}