nil_core/infrastructure/building/
mod.rs1pub 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 fn is_enabled(&self) -> bool;
23 fn toggle(&mut self, enabled: bool);
25
26 fn level(&self) -> BuildingLevel;
28 fn min_level(&self) -> BuildingLevel;
30 fn max_level(&self) -> BuildingLevel;
32 fn set_level(&mut self, level: BuildingLevel);
34
35 fn set_min_level(&mut self) {
37 self.set_level(self.min_level());
38 }
39
40 fn set_max_level(&mut self) {
42 self.set_level(self.max_level());
43 }
44
45 fn increase_level(&mut self) {
47 self.increase_level_by(1);
48 }
49
50 fn increase_level_by(&mut self, amount: u8);
52
53 fn decrease_level(&mut self) {
55 self.decrease_level_by(1);
56 }
57
58 fn decrease_level_by(&mut self, amount: u8);
60
61 fn is_min_level(&self) -> bool {
63 self.level() == self.min_level()
64 }
65
66 fn is_max_level(&self) -> bool {
68 self.level() >= self.max_level()
69 }
70
71 fn min_cost(&self) -> Cost;
73 fn max_cost(&self) -> Cost;
75
76 fn food_ratio(&self) -> ResourceRatio;
78 fn iron_ratio(&self) -> ResourceRatio;
80 fn stone_ratio(&self) -> ResourceRatio;
82 fn wood_ratio(&self) -> ResourceRatio;
84
85 fn maintenance(&self, stats: &BuildingStatsTable) -> Result<Maintenance>;
87 fn maintenance_ratio(&self) -> MaintenanceRatio;
89
90 fn min_workforce(&self) -> Workforce;
92 fn max_workforce(&self) -> Workforce;
94
95 fn score(&self, stats: &BuildingStatsTable) -> Result<Score>;
97 fn min_score(&self) -> Score;
99 fn max_score(&self) -> Score;
101
102 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#[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}