Skip to main content

nil_core/behavior/impl/
build.rs

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