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