Skip to main content

nil_core/infrastructure/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4pub mod building;
5pub mod catalog;
6pub mod mine;
7pub mod prelude;
8pub mod queue;
9pub mod requirements;
10pub mod stats;
11pub mod storage;
12
13use crate::error::Result;
14use crate::military::army::personnel::ArmyPersonnel;
15use crate::military::squad::Squad;
16use crate::ranking::score::Score;
17use crate::resources::Resources;
18use crate::resources::maintenance::Maintenance;
19use crate::world::config::WorldConfig;
20use bon::Builder;
21use building::r#impl::academy::recruit_queue::{
22  AcademyRecruitOrder,
23  AcademyRecruitOrderId,
24  AcademyRecruitOrderRequest,
25};
26use building::r#impl::prefecture::build_queue::{
27  PrefectureBuildOrder,
28  PrefectureBuildOrderKind,
29  PrefectureBuildOrderRequest,
30};
31use building::r#impl::stable::recruit_queue::{
32  StableRecruitOrder,
33  StableRecruitOrderId,
34  StableRecruitOrderRequest,
35};
36use building::r#impl::workshop::recruit_queue::{
37  WorkshopRecruitOrder,
38  WorkshopRecruitOrderId,
39  WorkshopRecruitOrderRequest,
40};
41use building::{Building, BuildingId, BuildingStatsTable, MineId, StorageId};
42use mine::Mine;
43use prelude::*;
44use requirements::InfrastructureRequirements;
45use serde::{Deserialize, Serialize};
46use stats::InfrastructureStats;
47use storage::Storage;
48use strum::IntoEnumIterator;
49use tap::Pipe;
50
51#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize)]
52#[serde(default, rename_all = "camelCase")]
53#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
54pub struct Infrastructure {
55  #[builder(default)]
56  academy: Academy,
57
58  #[builder(default)]
59  farm: Farm,
60
61  #[builder(default)]
62  iron_mine: IronMine,
63
64  #[builder(default)]
65  prefecture: Prefecture,
66
67  #[builder(default)]
68  quarry: Quarry,
69
70  #[builder(default)]
71  sawmill: Sawmill,
72
73  #[builder(default)]
74  silo: Silo,
75
76  #[builder(default)]
77  stable: Stable,
78
79  #[builder(default)]
80  wall: Wall,
81
82  #[builder(default)]
83  warehouse: Warehouse,
84
85  #[builder(default)]
86  workshop: Workshop,
87}
88
89impl Infrastructure {
90  #[inline]
91  pub fn new() -> Self {
92    Self::default()
93  }
94
95  pub const fn storage(&self, id: StorageId) -> &dyn Storage {
96    match id {
97      StorageId::Silo => &self.silo,
98      StorageId::Warehouse => &self.warehouse,
99    }
100  }
101
102  pub const fn mine(&self, id: MineId) -> &dyn Mine {
103    match id {
104      MineId::Farm => &self.farm,
105      MineId::IronMine => &self.iron_mine,
106      MineId::Quarry => &self.quarry,
107      MineId::Sawmill => &self.sawmill,
108    }
109  }
110
111  pub fn score(&self, stats: &InfrastructureStats) -> Result<Score> {
112    let mut score = Score::default();
113    for id in BuildingId::iter() {
114      let level = self.building(id).level();
115      if level > 0u8 {
116        let stats = stats.building(id)?;
117        score += stats.get(level)?.score;
118      }
119    }
120
121    Ok(score)
122  }
123
124  /// Determines the amount of resources generated by the mines at their current level,
125  /// before applying any modifiers, such as city stability.
126  pub fn round_base_production(&self, stats: &InfrastructureStats) -> Result<Resources> {
127    let mut resources = Resources::default();
128
129    macro_rules! set {
130      ($building:ident, $resource:ident) => {
131        paste::paste! {
132          let mine = &self.[<$building:snake>];
133          if mine.level() > 0u8 && mine.is_enabled() {
134            let mine_stats = stats.mine(MineId::$building)?;
135            resources.$resource = mine.production(mine_stats)?.into();
136          }
137        }
138      };
139    }
140
141    set!(Farm, food);
142    set!(IronMine, iron);
143    set!(Quarry, stone);
144    set!(Sawmill, wood);
145
146    Ok(resources)
147  }
148
149  pub(crate) fn add_prefecture_build_order(
150    &mut self,
151    request: &PrefectureBuildOrderRequest,
152    table: &BuildingStatsTable,
153    current_resources: Option<&Resources>,
154  ) -> Result<&PrefectureBuildOrder> {
155    let level = self.building(request.building).level();
156    self
157      .prefecture
158      .build_queue_mut()
159      .build(request, table, level, current_resources)
160  }
161
162  #[must_use]
163  pub(crate) fn cancel_prefecture_build_order(&mut self) -> Option<PrefectureBuildOrder> {
164    self.prefecture.build_queue_mut().cancel()
165  }
166
167  pub(crate) fn process_prefecture_build_queue(&mut self, config: &WorldConfig) {
168    if let Some(orders) = self.prefecture.process_queue(config) {
169      for order in orders {
170        let building = self.building_mut(order.building());
171        match order.kind() {
172          PrefectureBuildOrderKind::Construction => building.increase_level(),
173          PrefectureBuildOrderKind::Demolition => building.decrease_level(),
174        }
175      }
176    }
177  }
178}
179
180macro_rules! impl_infrastructure {
181  ($($building:ident),+) => {
182    paste::paste! {
183      impl Infrastructure {
184        $(
185          #[inline]
186          pub const fn [<$building:snake>](&self) -> &$building {
187            &self.[<$building:snake>]
188          }
189        )+
190
191        /// Creates a new instance with all buildings set to their maximum level.
192        pub fn with_max_level() -> Self {
193          Self {
194            $([<$building:snake>]: $building::with_max_level(),)+
195          }
196        }
197
198        pub const fn building(&self, id: BuildingId) -> &dyn Building {
199          match id {
200            $(BuildingId::$building => &self.[<$building:snake>],)+
201          }
202        }
203
204        pub(crate) const fn building_mut(&mut self, id: BuildingId) -> &mut dyn Building {
205          match id {
206            $(BuildingId::$building => &mut self.[<$building:snake>],)+
207          }
208        }
209
210        /// Determines the maintenance tax required for all buildings at their current levels.
211        pub fn base_maintenance(&self, stats: &InfrastructureStats) -> Result<Maintenance> {
212          let mut maintenance = Maintenance::default();
213          $(
214            let building = &self.[<$building:snake>];
215            if building.level() > 0u8 && building.is_enabled() {
216              let building_stats = stats.building(BuildingId::$building)?;
217              maintenance += building.maintenance(&building_stats)?;
218            }
219          )+
220
221          Ok(maintenance)
222        }
223
224        /// Determines whether the infrastructure meets the requirements.
225        pub fn has_required_levels(&self, requirements: &InfrastructureRequirements) -> bool {
226          $(self.[<$building:snake>].level() >= requirements.[<$building:snake>] &&)+ true
227        }
228      }
229    }
230  };
231}
232
233impl_infrastructure!(
234  Academy, Farm, IronMine, Prefecture, Quarry, Sawmill, Silo, Stable, Wall, Warehouse, Workshop
235);
236
237macro_rules! impl_recruitment {
238  ($building:ident) => {
239    paste::paste! {
240      impl Infrastructure {
241        pub(crate) fn [<add_ $building:snake _recruit_order>](
242          &mut self,
243          request: &[<$building RecruitOrderRequest>],
244          current_resources: Option<&Resources>,
245        ) -> Result<&[<$building RecruitOrder>]> {
246          self
247            .[<$building:snake>]
248            .recruit_queue_mut()
249            .recruit(request, current_resources)
250        }
251
252        #[must_use]
253        pub(crate) fn [<cancel_ $building:snake _recruit_order>](
254          &mut self,
255          id: [<$building RecruitOrderId>]
256        ) -> Option<[<$building RecruitOrder>]> {
257          self.[<$building:snake>].recruit_queue_mut().cancel(id)
258        }
259
260        #[must_use]
261        pub(crate) fn [<process_ $building:snake _recruit_queue>](
262          &mut self,
263          config: &WorldConfig
264        ) -> Option<ArmyPersonnel> {
265          self
266            .[<$building:snake>]
267            .process_queue(config)?
268            .into_iter()
269            .map(Squad::from)
270            .collect::<ArmyPersonnel>()
271            .pipe(Some)
272        }
273      }
274    }
275  };
276}
277
278impl_recruitment!(Academy);
279impl_recruitment!(Stable);
280impl_recruitment!(Workshop);