use crate::behavior::r#impl::idle::IdleBehavior;
use crate::behavior::score::BehaviorScore;
use crate::behavior::{Behavior, BehaviorProcessor};
use crate::continent::Coord;
use crate::error::Result;
use crate::ethic::EthicPowerAxis;
use crate::infrastructure::building::StorageId;
use crate::infrastructure::building::r#impl::prefecture::build_queue::{
PrefectureBuildOrderKind,
PrefectureBuildOrderRequest,
};
use crate::infrastructure::prelude::*;
use crate::infrastructure::queue::InfrastructureQueue;
use crate::military::maneuver::Maneuver;
use crate::world::World;
use bon::Builder;
use nil_util::iter::IterExt;
use rand::random_range;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::marker::PhantomData;
use std::ops::ControlFlow;
use std::sync::LazyLock;
use strum::IntoEnumIterator;
pub(crate) static BUILD_TEMPLATE: LazyLock<Vec<BuildStep>> = LazyLock::new(generate_template);
#[derive(Builder, Debug)]
pub struct BuildBehavior {
coord: Coord,
}
impl BuildBehavior {
const MAX_IN_QUEUE: u8 = 5;
}
impl Behavior for BuildBehavior {
fn score(&self, world: &World) -> Result<BehaviorScore> {
let config = world.config();
let infrastructure = world.infrastructure(self.coord)?;
let max_in_queue = f64::from(Self::MAX_IN_QUEUE);
if let Some(in_queue) = infrastructure
.prefecture()
.turns_in_build_queue(&config)
{
Ok(BehaviorScore::new(1.0 - (in_queue / max_in_queue)))
} else {
Ok(BehaviorScore::MIN)
}
}
fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
let mut behaviors = vec![IdleBehavior.boxed()];
macro_rules! push {
($building:ident, $id:expr) => {{
let behavior = BuildBuildingBehavior::builder()
.marker(PhantomData::<$building>)
.coord(self.coord)
.building($id)
.build()
.boxed();
behaviors.push(behavior);
}};
}
for id in BuildingId::iter() {
match id {
BuildingId::Academy => push!(Academy, id),
BuildingId::Farm => push!(Farm, id),
BuildingId::IronMine => push!(IronMine, id),
BuildingId::Prefecture => push!(Prefecture, id),
BuildingId::Quarry => push!(Quarry, id),
BuildingId::Sawmill => push!(Sawmill, id),
BuildingId::Silo => push!(Silo, id),
BuildingId::Stable => push!(Stable, id),
BuildingId::Wall => push!(Wall, id),
BuildingId::Warehouse => push!(Warehouse, id),
BuildingId::Workshop => push!(Workshop, id),
}
}
BehaviorProcessor::new(world, behaviors)
.take(usize::from(Self::MAX_IN_QUEUE))
.try_each()?;
Ok(ControlFlow::Break(()))
}
}
#[derive(Builder, Debug)]
pub struct BuildBuildingBehavior<T>
where
T: Building + Debug,
{
coord: Coord,
building: BuildingId,
marker: PhantomData<T>,
}
impl<T> BuildBuildingBehavior<T>
where
T: Building + Debug,
{
const STORAGE_CAPACITY_THRESHOLD: f64 = 0.8;
}
impl<T> Behavior for BuildBuildingBehavior<T>
where
T: Building + Debug + 'static,
{
#[allow(clippy::too_many_lines)]
fn score(&self, world: &World) -> Result<BehaviorScore> {
let infrastructure = world.infrastructure(self.coord)?;
let building = infrastructure.building(self.building);
if !building
.infrastructure_requirements()
.has_required_levels(infrastructure)
{
return Ok(BehaviorScore::MIN);
}
let level = infrastructure
.prefecture()
.resolve_level(self.building, building.level());
if level >= building.max_level() {
return Ok(BehaviorScore::MIN);
}
let stats = world.stats().infrastructure();
let owner = world.continent().owner_of(self.coord)?;
let ruler_ref = world.ruler(owner)?;
let required_resources = &stats
.building(self.building)?
.get(level + 1u8)?
.resources;
if !ruler_ref.has_resources(required_resources) {
return Ok(BehaviorScore::MIN);
}
if let BuildingId::Wall = self.building
&& let Some(distance) = world
.military()
.maneuvers()
.filter(|maneuver| maneuver.destination() == self.coord)
.filter(|maneuver| maneuver.is_attack() && maneuver.is_going())
.filter_map(Maneuver::pending_distance)
.min()
{
let workforce = stats
.building(self.building)?
.get(level + 1u8)
.map(|it| f64::from(it.workforce))?;
if workforce <= f64::from(distance) {
return Ok(BehaviorScore::MAX);
}
}
if let BuildingId::Farm = self.building
&& !world
.get_maintenance_balance(owner.clone())?
.is_sustainable()
{
return Ok(BehaviorScore::MAX);
}
if let Ok(id) = StorageId::try_from(self.building) {
let resources = ruler_ref.resources();
let capacity = world.get_storage_capacity(owner.clone())?;
let ratio = match id {
StorageId::Silo => f64::from(resources.food) / f64::from(capacity.silo),
StorageId::Warehouse => {
let capacity = f64::from(capacity.warehouse);
let iron_ratio = f64::from(resources.iron) / capacity;
let stone_ratio = f64::from(resources.stone) / capacity;
let wood_ratio = f64::from(resources.wood) / capacity;
iron_ratio.max(stone_ratio).max(wood_ratio)
}
};
if ratio >= Self::STORAGE_CAPACITY_THRESHOLD
&& infrastructure
.prefecture()
.build_queue()
.iter()
.filter(|order| order.kind().is_construction())
.all(|order| order.building() != self.building)
{
return Ok(BehaviorScore::MAX);
}
}
let mut score = if BUILD_TEMPLATE
.iter()
.filter(|step| !step.is_done(infrastructure))
.take(3)
.any(|step| step.id == self.building)
{
BehaviorScore::new(random_range(0.8..=1.0))
} else {
BehaviorScore::MIN
};
if let Some(ethics) = ruler_ref.ethics() {
if self.building.is_civil() {
score *= match ethics.power() {
EthicPowerAxis::Militarist => 0.9,
EthicPowerAxis::FanaticMilitarist => 0.75,
EthicPowerAxis::Pacifist => 1.1,
EthicPowerAxis::FanaticPacifist => 1.25,
}
} else {
score *= match ethics.power() {
EthicPowerAxis::Militarist => 1.1,
EthicPowerAxis::FanaticMilitarist => 1.25,
EthicPowerAxis::Pacifist => 0.9,
EthicPowerAxis::FanaticPacifist => 0.75,
}
}
}
Ok(score)
}
fn behave(&self, world: &mut World) -> Result<ControlFlow<()>> {
let order = PrefectureBuildOrderRequest {
coord: self.coord,
building: self.building,
kind: PrefectureBuildOrderKind::Construction,
};
world.add_prefecture_build_order(&order)?;
Ok(ControlFlow::Continue(()))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct BuildStep {
id: BuildingId,
level: BuildingLevel,
}
impl BuildStep {
const fn new(id: BuildingId, level: BuildingLevel) -> Self {
Self { id, level }
}
pub fn is_done(&self, infrastructure: &Infrastructure) -> bool {
self.level <= infrastructure.building(self.id).level()
}
}
macro_rules! step {
($id:ident, $level: expr) => {{ BuildStep::new(BuildingId::$id, BuildingLevel::new($level)) }};
}
fn generate_template() -> Vec<BuildStep> {
vec![
step!(Sawmill, 8),
step!(Quarry, 8),
step!(IronMine, 8),
step!(Prefecture, 2),
step!(Sawmill, 10),
step!(Quarry, 10),
step!(IronMine, 10),
step!(Prefecture, 3),
step!(Academy, 1),
step!(Farm, 2),
step!(Wall, 1),
step!(Warehouse, 2),
step!(Silo, 2),
step!(Sawmill, 12),
step!(Quarry, 12),
step!(IronMine, 12),
step!(Wall, 3),
step!(Academy, 3),
step!(Silo, 4),
step!(Farm, 4),
step!(Prefecture, 5),
step!(Sawmill, 15),
step!(Quarry, 15),
step!(IronMine, 15),
step!(Academy, 5),
step!(Warehouse, 11),
step!(Wall, 7),
step!(Prefecture, 10),
step!(Stable, 1),
step!(Sawmill, 18),
step!(Quarry, 18),
step!(IronMine, 18),
step!(Stable, 2),
step!(Wall, 10),
step!(Silo, 6),
step!(Farm, 6),
step!(Stable, 3),
step!(Warehouse, 15),
step!(Wall, 15),
step!(Sawmill, 20),
step!(Quarry, 20),
step!(IronMine, 20),
step!(Silo, 10),
step!(Farm, 10),
step!(Warehouse, 18),
step!(Wall, 20),
step!(Prefecture, 17),
step!(Stable, 6),
step!(Academy, 8),
step!(Sawmill, 25),
step!(Quarry, 25),
step!(IronMine, 25),
step!(Warehouse, 20),
step!(Silo, 15),
step!(Farm, 15),
step!(Stable, 13),
step!(Academy, 13),
step!(Warehouse, 23),
step!(Prefecture, 25),
step!(Academy, 20),
step!(Silo, 20),
step!(Farm, 20),
step!(Warehouse, 27),
step!(Sawmill, 30),
step!(Quarry, 30),
step!(IronMine, 30),
step!(Silo, 25),
step!(Farm, 25),
step!(Stable, 20),
step!(Academy, 25),
step!(Prefecture, 30),
step!(Warehouse, 30),
step!(Farm, 30),
step!(Silo, 30),
]
}