Skip to main content

nil_core/behavior/
build.rs

1// Copyright (C) Call of Nil contributors
2// SPDX-License-Identifier: AGPL-3.0-only
3
4use crate::behavior::idle::IdleBehavior;
5use crate::behavior::{Behavior, BehaviorProcessor, BehaviorScore};
6use crate::continent::Coord;
7use crate::error::Result;
8use crate::ethic::EthicPowerAxis;
9use crate::infrastructure::building::StorageId;
10use crate::infrastructure::building::prefecture::build_queue::{
11  PrefectureBuildOrderKind,
12  PrefectureBuildOrderRequest,
13};
14use crate::infrastructure::prelude::*;
15use crate::infrastructure::queue::InfrastructureQueue;
16use crate::military::maneuver::Maneuver;
17use crate::world::World;
18use bon::Builder;
19use nil_util::iter::IterExt;
20use rand::random_range;
21use serde::{Deserialize, Serialize};
22use std::fmt::Debug;
23use std::marker::PhantomData;
24use std::ops::ControlFlow;
25use std::sync::LazyLock;
26use strum::IntoEnumIterator;
27
28pub(crate) static BUILD_TEMPLATE: LazyLock<Vec<BuildStep>> = LazyLock::new(generate_template);
29
30#[derive(Builder, Debug)]
31pub struct BuildBehavior {
32  coord: Coord,
33}
34
35impl BuildBehavior {
36  const MAX_IN_QUEUE: u8 = 3;
37}
38
39impl Behavior for BuildBehavior {
40  fn score(&self, world: &World) -> Result<BehaviorScore> {
41    let infrastructure = world.infrastructure(self.coord)?;
42    let max_in_queue = f64::from(Self::MAX_IN_QUEUE);
43
44    if let Some(in_queue) = infrastructure
45      .prefecture()
46      .turns_in_build_queue()
47    {
48      Ok(BehaviorScore::new(1.0 - (in_queue / max_in_queue)))
49    } else {
50      Ok(BehaviorScore::MIN)
51    }
52  }
53
54  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
55    let mut behaviors = vec![IdleBehavior.boxed()];
56    macro_rules! push {
57      ($building:ident, $id:expr) => {{
58        let behavior = BuildBuildingBehavior::builder()
59          .marker(PhantomData::<$building>)
60          .coord(self.coord)
61          .building($id)
62          .build()
63          .boxed();
64
65        behaviors.push(behavior);
66      }};
67    }
68
69    for id in BuildingId::iter() {
70      match id {
71        BuildingId::Academy => push!(Academy, id),
72        BuildingId::Farm => push!(Farm, id),
73        BuildingId::IronMine => push!(IronMine, id),
74        BuildingId::Prefecture => push!(Prefecture, id),
75        BuildingId::Quarry => push!(Quarry, id),
76        BuildingId::Sawmill => push!(Sawmill, id),
77        BuildingId::Silo => push!(Silo, id),
78        BuildingId::Stable => push!(Stable, id),
79        BuildingId::Wall => push!(Wall, id),
80        BuildingId::Warehouse => push!(Warehouse, id),
81        BuildingId::Workshop => push!(Workshop, id),
82      }
83    }
84
85    BehaviorProcessor::new(world, behaviors)
86      .take(usize::from(Self::MAX_IN_QUEUE))
87      .try_each()?;
88
89    Ok(ControlFlow::Break(()))
90  }
91}
92
93#[derive(Builder, Debug)]
94pub struct BuildBuildingBehavior<T>
95where
96  T: Building + Debug,
97{
98  coord: Coord,
99  building: BuildingId,
100  marker: PhantomData<T>,
101}
102
103impl<T> BuildBuildingBehavior<T>
104where
105  T: Building + Debug,
106{
107  const STORAGE_CAPACITY_THRESHOLD: f64 = 0.8;
108}
109
110impl<T> Behavior for BuildBuildingBehavior<T>
111where
112  T: Building + Debug + 'static,
113{
114  #[allow(clippy::too_many_lines)]
115  fn score(&self, world: &World) -> Result<BehaviorScore> {
116    let infrastructure = world.infrastructure(self.coord)?;
117    let building = infrastructure.building(self.building);
118
119    if !building
120      .infrastructure_requirements()
121      .has_required_levels(infrastructure)
122    {
123      return Ok(BehaviorScore::MIN);
124    }
125
126    let level = infrastructure
127      .prefecture()
128      .resolve_level(self.building, building.level());
129
130    if level >= building.max_level() {
131      return Ok(BehaviorScore::MIN);
132    }
133
134    let stats = world.stats().infrastructure();
135    let owner = world.continent().owner_of(self.coord)?;
136    let ruler_ref = world.ruler(owner)?;
137
138    let required_resources = &stats
139      .building(self.building)?
140      .get(level + 1u8)?
141      .resources;
142
143    if !ruler_ref.has_resources(required_resources) {
144      return Ok(BehaviorScore::MIN);
145    }
146
147    // Prioritize the wall if there are incoming attacks.
148    // It will also check whether the build order can be
149    // completed before the nearest one arrives.
150    if let BuildingId::Wall = self.building
151      && let Some(distance) = world
152        .military()
153        .maneuvers()
154        .filter(|maneuver| maneuver.destination() == self.coord)
155        .filter(|maneuver| maneuver.is_attack() && maneuver.is_going())
156        .filter_map(Maneuver::pending_distance)
157        .min()
158    {
159      let workforce = stats
160        .building(self.building)?
161        .get(level + 1u8)
162        .map(|it| f64::from(it.workforce))?;
163
164      if workforce <= f64::from(distance) {
165        return Ok(BehaviorScore::MAX);
166      }
167    }
168
169    if let BuildingId::Farm = self.building
170      && !world
171        .get_maintenance_balance(owner.clone())?
172        .is_sustainable()
173    {
174      return Ok(BehaviorScore::MAX);
175    }
176
177    // Prioritize storage when its capacity is almost full.
178    if let Ok(id) = StorageId::try_from(self.building) {
179      let resources = ruler_ref.resources();
180      let capacity = world.get_storage_capacity(owner.clone())?;
181
182      let ratio = match id {
183        StorageId::Silo => f64::from(resources.food) / f64::from(capacity.silo),
184        StorageId::Warehouse => {
185          let capacity = f64::from(capacity.warehouse);
186          let iron_ratio = f64::from(resources.iron) / capacity;
187          let stone_ratio = f64::from(resources.stone) / capacity;
188          let wood_ratio = f64::from(resources.wood) / capacity;
189          iron_ratio.max(stone_ratio).max(wood_ratio)
190        }
191      };
192
193      if ratio >= Self::STORAGE_CAPACITY_THRESHOLD
194        && infrastructure
195          .prefecture()
196          .build_queue()
197          .iter()
198          .filter(|order| order.kind().is_construction())
199          .all(|order| order.building() != self.building)
200      {
201        return Ok(BehaviorScore::MAX);
202      }
203    }
204
205    let mut score = if BUILD_TEMPLATE
206      .iter()
207      .filter(|step| !step.is_done(infrastructure))
208      .take(3)
209      .any(|step| step.id == self.building)
210    {
211      BehaviorScore::new(random_range(0.8..=1.0))
212    } else {
213      BehaviorScore::MIN
214    };
215
216    if let Some(ethics) = ruler_ref.ethics() {
217      if self.building.is_civil() {
218        score *= match ethics.power() {
219          EthicPowerAxis::Militarist => 0.9,
220          EthicPowerAxis::FanaticMilitarist => 0.75,
221          EthicPowerAxis::Pacifist => 1.1,
222          EthicPowerAxis::FanaticPacifist => 1.25,
223        }
224      } else {
225        score *= match ethics.power() {
226          EthicPowerAxis::Militarist => 1.1,
227          EthicPowerAxis::FanaticMilitarist => 1.25,
228          EthicPowerAxis::Pacifist => 0.9,
229          EthicPowerAxis::FanaticPacifist => 0.75,
230        }
231      }
232    }
233
234    Ok(score)
235  }
236
237  fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
238    let order = PrefectureBuildOrderRequest {
239      coord: self.coord,
240      building: self.building,
241      kind: PrefectureBuildOrderKind::Construction,
242    };
243
244    world.add_prefecture_build_order(&order)?;
245
246    Ok(ControlFlow::Continue(()))
247  }
248}
249
250#[derive(Clone, Debug, Deserialize, Serialize)]
251#[serde(rename_all = "camelCase")]
252pub struct BuildStep {
253  id: BuildingId,
254  level: BuildingLevel,
255}
256
257impl BuildStep {
258  fn new(id: BuildingId, level: BuildingLevel) -> Self {
259    Self { id, level }
260  }
261
262  pub fn is_done(&self, infrastructure: &Infrastructure) -> bool {
263    self.level <= infrastructure.building(self.id).level()
264  }
265}
266
267macro_rules! step {
268  ($id:ident, $level: expr) => {{ BuildStep::new(BuildingId::$id, BuildingLevel::new($level)) }};
269}
270
271fn generate_template() -> Vec<BuildStep> {
272  vec![
273    step!(Sawmill, 8),
274    step!(Quarry, 8),
275    step!(IronMine, 8),
276    step!(Prefecture, 2),
277    step!(Sawmill, 10),
278    step!(Quarry, 10),
279    step!(IronMine, 10),
280    step!(Prefecture, 3),
281    step!(Academy, 1),
282    step!(Farm, 2),
283    step!(Wall, 1),
284    step!(Warehouse, 2),
285    step!(Silo, 2),
286    step!(Sawmill, 12),
287    step!(Quarry, 12),
288    step!(IronMine, 12),
289    step!(Wall, 3),
290    step!(Academy, 3),
291    step!(Silo, 4),
292    step!(Farm, 4),
293    step!(Prefecture, 5),
294    step!(Sawmill, 15),
295    step!(Quarry, 15),
296    step!(IronMine, 15),
297    step!(Academy, 5),
298    step!(Warehouse, 11),
299    step!(Wall, 7),
300    step!(Prefecture, 10),
301    step!(Stable, 1),
302    step!(Sawmill, 18),
303    step!(Quarry, 18),
304    step!(IronMine, 18),
305    step!(Stable, 2),
306    step!(Wall, 10),
307    step!(Silo, 6),
308    step!(Farm, 6),
309    step!(Stable, 3),
310    step!(Warehouse, 15),
311    step!(Wall, 15),
312    step!(Sawmill, 20),
313    step!(Quarry, 20),
314    step!(IronMine, 20),
315    step!(Silo, 10),
316    step!(Farm, 10),
317    step!(Warehouse, 18),
318    step!(Wall, 20),
319    step!(Prefecture, 17),
320    step!(Stable, 6),
321    step!(Academy, 8),
322    step!(Sawmill, 25),
323    step!(Quarry, 25),
324    step!(IronMine, 25),
325    step!(Warehouse, 20),
326    step!(Silo, 15),
327    step!(Farm, 15),
328    step!(Stable, 13),
329    step!(Academy, 13),
330    step!(Warehouse, 23),
331    step!(Prefecture, 25),
332    step!(Academy, 20),
333    step!(Silo, 20),
334    step!(Farm, 20),
335    step!(Warehouse, 27),
336    step!(Sawmill, 30),
337    step!(Quarry, 30),
338    step!(IronMine, 30),
339    step!(Silo, 25),
340    step!(Farm, 25),
341    step!(Stable, 20),
342    step!(Academy, 25),
343    step!(Prefecture, 30),
344    step!(Warehouse, 30),
345    step!(Farm, 30),
346    step!(Silo, 30),
347  ]
348}