Skip to main content

nil_core/infrastructure/building/
mod.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4pub mod r#impl;
5pub mod level;
6
7use crate::error::{Error, Result};
8use crate::infrastructure::building::level::BuildingLevel;
9use crate::infrastructure::requirements::InfrastructureRequirements;
10use crate::ranking::score::Score;
11use crate::resources::prelude::*;
12use nil_num::growth::growth;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use strum::{EnumIs, EnumIter};
16use subenum::subenum;
17
18pub trait Building: Send + Sync {
19  fn id(&self) -> BuildingId;
20
21  /// Checks whether the building is enabled.
22  fn is_enabled(&self) -> bool;
23  /// Enables or disables the building.
24  fn toggle(&mut self, enabled: bool);
25
26  /// Current building level.
27  fn level(&self) -> BuildingLevel;
28  /// Minimum building level.
29  fn min_level(&self) -> BuildingLevel;
30  /// Maximum building level.
31  fn max_level(&self) -> BuildingLevel;
32  /// Sets the building's level while ensuring it remains within the level limit.
33  fn set_level(&mut self, level: BuildingLevel);
34
35  /// Sets the building to its **minimum** level.
36  fn set_min_level(&mut self) {
37    self.set_level(self.min_level());
38  }
39
40  /// Sets the building to its **maximum** level.
41  fn set_max_level(&mut self) {
42    self.set_level(self.max_level());
43  }
44
45  /// Increases the building level by one, if possible.
46  fn increase_level(&mut self) {
47    self.increase_level_by(1);
48  }
49
50  /// Increases the level of the building by a certain amount, if possible.
51  fn increase_level_by(&mut self, amount: u8);
52
53  /// Decreases the building level by one, if possible.
54  fn decrease_level(&mut self) {
55    self.decrease_level_by(1);
56  }
57
58  /// Decreases the level of the building by a certain amount, if possible.
59  fn decrease_level_by(&mut self, amount: u8);
60
61  /// Checks whether the building is at its minimum level.
62  fn is_min_level(&self) -> bool {
63    self.level() == self.min_level()
64  }
65
66  /// Checks whether the building is at its maximum level.
67  fn is_max_level(&self) -> bool {
68    self.level() >= self.max_level()
69  }
70
71  /// Total cost for the **minimum** level of the building.
72  fn min_cost(&self) -> Cost;
73  /// Total cost for the **maximum** level of the building.
74  fn max_cost(&self) -> Cost;
75
76  /// Percentage of the total cost related to food.
77  fn food_ratio(&self) -> ResourceRatio;
78  /// Percentage of the total cost related to iron.
79  fn iron_ratio(&self) -> ResourceRatio;
80  /// Percentage of the total cost related to stone.
81  fn stone_ratio(&self) -> ResourceRatio;
82  /// Percentage of the total cost related to wood.
83  fn wood_ratio(&self) -> ResourceRatio;
84
85  /// Building maintenance tax at its current level.
86  fn maintenance(&self, stats: &BuildingStatsTable) -> Result<Maintenance>;
87  /// Proportion of the base cost used as a maintenance tax.
88  fn maintenance_ratio(&self) -> MaintenanceRatio;
89
90  /// Workforce required for the **minimum** level of the building.
91  fn min_workforce(&self) -> Workforce;
92  /// Workforce required for the **maximum** level of the building.
93  fn max_workforce(&self) -> Workforce;
94
95  // Current score.
96  fn score(&self, stats: &BuildingStatsTable) -> Result<Score>;
97  // Building score at its **minimum** level.
98  fn min_score(&self) -> Score;
99  // Building score at its **maximum** level.
100  fn max_score(&self) -> Score;
101
102  /// Levels required to construct the building.
103  fn infrastructure_requirements(&self) -> &InfrastructureRequirements;
104
105  fn is_civil(&self) -> bool {
106    self.id().is_civil()
107  }
108
109  fn is_military(&self) -> bool {
110    self.id().is_military()
111  }
112
113  fn is_mine(&self) -> bool {
114    self.id().is_mine()
115  }
116
117  fn is_storage(&self) -> bool {
118    self.id().is_storage()
119  }
120}
121
122#[subenum(CivilBuildingId, MilitaryBuildingId, MineId, StorageId)]
123#[derive(
124  Clone, Copy, Debug, strum::Display, EnumIs, EnumIter, PartialEq, Eq, Hash, Deserialize, Serialize,
125)]
126#[serde(rename_all = "kebab-case")]
127#[strum(serialize_all = "kebab-case")]
128#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
129pub enum BuildingId {
130  #[subenum(MilitaryBuildingId)]
131  Academy,
132
133  #[subenum(CivilBuildingId, MineId)]
134  Farm,
135
136  #[subenum(CivilBuildingId, MineId)]
137  IronMine,
138
139  #[subenum(CivilBuildingId)]
140  Prefecture,
141
142  #[subenum(CivilBuildingId, MineId)]
143  Quarry,
144
145  #[subenum(CivilBuildingId, MineId)]
146  Sawmill,
147
148  #[subenum(CivilBuildingId, StorageId)]
149  Silo,
150
151  #[subenum(MilitaryBuildingId)]
152  Stable,
153
154  Wall,
155
156  #[subenum(CivilBuildingId, StorageId)]
157  Warehouse,
158
159  #[subenum(MilitaryBuildingId)]
160  Workshop,
161}
162
163impl BuildingId {
164  #[inline]
165  pub fn is_civil(self) -> bool {
166    CivilBuildingId::try_from(self).is_ok()
167  }
168
169  #[inline]
170  pub fn is_military(self) -> bool {
171    MilitaryBuildingId::try_from(self).is_ok()
172  }
173
174  #[inline]
175  pub fn is_mine(self) -> bool {
176    MineId::try_from(self).is_ok()
177  }
178
179  #[inline]
180  pub fn is_storage(self) -> bool {
181    StorageId::try_from(self).is_ok()
182  }
183}
184
185/// Information about a building at a given level.
186#[derive(Clone, Debug, Deserialize, Serialize)]
187#[serde(rename_all = "camelCase")]
188#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
189pub struct BuildingStats {
190  pub level: BuildingLevel,
191  pub cost: Cost,
192  pub resources: Resources,
193  pub maintenance: Maintenance,
194  pub workforce: Workforce,
195  pub score: Score,
196}
197
198#[derive(Clone, Debug, Deserialize, Serialize)]
199#[serde(rename_all = "camelCase")]
200#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
201pub struct BuildingStatsTable {
202  id: BuildingId,
203  min_level: BuildingLevel,
204  max_level: BuildingLevel,
205  table: HashMap<BuildingLevel, BuildingStats>,
206}
207
208impl BuildingStatsTable {
209  pub(crate) fn new(building: &dyn Building) -> Self {
210    let min_level = building.min_level();
211    let max_level = building.max_level();
212    let mut table = HashMap::with_capacity(max_level.into());
213
214    let mut cost = f64::from(building.min_cost());
215    let cost_growth = growth()
216      .floor(cost)
217      .ceil(building.max_cost())
218      .max_level(max_level)
219      .call();
220
221    let mut workforce = f64::from(building.min_workforce());
222    let workforce_growth = growth()
223      .floor(workforce)
224      .ceil(building.max_workforce())
225      .max_level(max_level)
226      .call();
227
228    let mut score = f64::from(building.min_score());
229    let score_growth = growth()
230      .floor(score)
231      .ceil(building.max_score())
232      .max_level(max_level)
233      .call();
234
235    let food_ratio = *building.food_ratio();
236    let iron_ratio = *building.iron_ratio();
237    let stone_ratio = *building.stone_ratio();
238    let wood_ratio = *building.wood_ratio();
239
240    let maintenance_ratio = *building.maintenance_ratio();
241    let mut maintenance = cost * maintenance_ratio;
242
243    for level in 1..=u8::from(max_level) {
244      let level = BuildingLevel::new(level);
245      let resources = Resources {
246        food: Food::from((cost * food_ratio).round()),
247        iron: Iron::from((cost * iron_ratio).round()),
248        stone: Stone::from((cost * stone_ratio).round()),
249        wood: Wood::from((cost * wood_ratio).round()),
250      };
251
252      table.insert(
253        level,
254        BuildingStats {
255          level,
256          cost: Cost::from(cost.round()),
257          resources,
258          maintenance: Maintenance::from(maintenance.round()),
259          workforce: Workforce::from(workforce.round()),
260          score: Score::from(score.round()),
261        },
262      );
263
264      debug_assert!(cost.is_normal());
265      debug_assert!(workforce.is_normal());
266
267      debug_assert!(maintenance.is_finite());
268      debug_assert!(maintenance >= 0.0);
269
270      debug_assert!(score.is_finite());
271      debug_assert!(score >= 0.0);
272
273      cost += cost * cost_growth;
274      workforce += workforce * workforce_growth;
275      score += score * score_growth;
276
277      maintenance = cost * maintenance_ratio;
278    }
279
280    table.shrink_to_fit();
281
282    Self {
283      id: building.id(),
284      min_level,
285      max_level,
286      table,
287    }
288  }
289
290  #[inline]
291  pub fn id(&self) -> BuildingId {
292    self.id
293  }
294
295  #[inline]
296  pub fn min_level(&self) -> BuildingLevel {
297    self.min_level
298  }
299
300  #[inline]
301  pub fn max_level(&self) -> BuildingLevel {
302    self.max_level
303  }
304
305  #[inline]
306  pub fn get(&self, level: BuildingLevel) -> Result<&BuildingStats> {
307    self
308      .table
309      .get(&level)
310      .ok_or(Error::BuildingStatsNotFoundForLevel(self.id, level))
311  }
312}