nil-core 0.5.1

Multiplayer strategy game
Documentation
// Copyright (C) Call of Nil contributors
// SPDX-License-Identifier: AGPL-3.0-only

pub mod building;
pub mod catalog;
pub mod mine;
pub mod prelude;
pub mod queue;
pub mod requirements;
pub mod stats;
pub mod storage;

use crate::error::Result;
use crate::military::army::personnel::ArmyPersonnel;
use crate::military::squad::Squad;
use crate::ranking::score::Score;
use crate::resources::Resources;
use crate::resources::maintenance::Maintenance;
use crate::world::config::WorldConfig;
use bon::Builder;
use building::r#impl::academy::recruit_queue::{
  AcademyRecruitOrder,
  AcademyRecruitOrderId,
  AcademyRecruitOrderRequest,
};
use building::r#impl::prefecture::build_queue::{
  PrefectureBuildOrder,
  PrefectureBuildOrderKind,
  PrefectureBuildOrderRequest,
};
use building::r#impl::stable::recruit_queue::{
  StableRecruitOrder,
  StableRecruitOrderId,
  StableRecruitOrderRequest,
};
use building::r#impl::workshop::recruit_queue::{
  WorkshopRecruitOrder,
  WorkshopRecruitOrderId,
  WorkshopRecruitOrderRequest,
};
use building::{Building, BuildingId, BuildingStatsTable, MineId, StorageId};
use mine::Mine;
use prelude::*;
use requirements::InfrastructureRequirements;
use serde::{Deserialize, Serialize};
use stats::InfrastructureStats;
use storage::Storage;
use strum::IntoEnumIterator;
use tap::Pipe;

#[derive(Builder, Clone, Debug, Default, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct Infrastructure {
  #[builder(default)]
  academy: Academy,

  #[builder(default)]
  farm: Farm,

  #[builder(default)]
  iron_mine: IronMine,

  #[builder(default)]
  prefecture: Prefecture,

  #[builder(default)]
  quarry: Quarry,

  #[builder(default)]
  sawmill: Sawmill,

  #[builder(default)]
  silo: Silo,

  #[builder(default)]
  stable: Stable,

  #[builder(default)]
  wall: Wall,

  #[builder(default)]
  warehouse: Warehouse,

  #[builder(default)]
  workshop: Workshop,
}

impl Infrastructure {
  #[inline]
  pub fn new() -> Self {
    Self::default()
  }

  pub const fn storage(&self, id: StorageId) -> &dyn Storage {
    match id {
      StorageId::Silo => &self.silo,
      StorageId::Warehouse => &self.warehouse,
    }
  }

  pub const fn mine(&self, id: MineId) -> &dyn Mine {
    match id {
      MineId::Farm => &self.farm,
      MineId::IronMine => &self.iron_mine,
      MineId::Quarry => &self.quarry,
      MineId::Sawmill => &self.sawmill,
    }
  }

  pub fn score(&self, stats: &InfrastructureStats) -> Result<Score> {
    let mut score = Score::default();
    for id in BuildingId::iter() {
      let level = self.building(id).level();
      if level > 0u8 {
        let stats = stats.building(id)?;
        score += stats.get(level)?.score;
      }
    }

    Ok(score)
  }

  /// Determines the amount of resources generated by the mines at their current level,
  /// before applying any modifiers, such as city stability.
  pub fn round_base_production(&self, stats: &InfrastructureStats) -> Result<Resources> {
    let mut resources = Resources::default();

    macro_rules! set {
      ($building:ident, $resource:ident) => {
        paste::paste! {
          let mine = &self.[<$building:snake>];
          if mine.level() > 0u8 && mine.is_enabled() {
            let mine_stats = stats.mine(MineId::$building)?;
            resources.$resource = mine.production(mine_stats)?.into();
          }
        }
      };
    }

    set!(Farm, food);
    set!(IronMine, iron);
    set!(Quarry, stone);
    set!(Sawmill, wood);

    Ok(resources)
  }

  pub(crate) fn add_prefecture_build_order(
    &mut self,
    request: &PrefectureBuildOrderRequest,
    table: &BuildingStatsTable,
    current_resources: Option<&Resources>,
  ) -> Result<&PrefectureBuildOrder> {
    let level = self.building(request.building).level();
    self
      .prefecture
      .build_queue_mut()
      .build(request, table, level, current_resources)
  }

  #[must_use]
  pub(crate) fn cancel_prefecture_build_order(&mut self) -> Option<PrefectureBuildOrder> {
    self.prefecture.build_queue_mut().cancel()
  }

  pub(crate) fn process_prefecture_build_queue(&mut self, config: &WorldConfig) {
    if let Some(orders) = self.prefecture.process_queue(config) {
      for order in orders {
        let building = self.building_mut(order.building());
        match order.kind() {
          PrefectureBuildOrderKind::Construction => building.increase_level(),
          PrefectureBuildOrderKind::Demolition => building.decrease_level(),
        }
      }
    }
  }
}

macro_rules! impl_infrastructure {
  ($($building:ident),+) => {
    paste::paste! {
      impl Infrastructure {
        $(
          #[inline]
          pub const fn [<$building:snake>](&self) -> &$building {
            &self.[<$building:snake>]
          }
        )+

        /// Creates a new instance with all buildings set to their maximum level.
        pub fn with_max_level() -> Self {
          Self {
            $([<$building:snake>]: $building::with_max_level(),)+
          }
        }

        pub const fn building(&self, id: BuildingId) -> &dyn Building {
          match id {
            $(BuildingId::$building => &self.[<$building:snake>],)+
          }
        }

        pub(crate) const fn building_mut(&mut self, id: BuildingId) -> &mut dyn Building {
          match id {
            $(BuildingId::$building => &mut self.[<$building:snake>],)+
          }
        }

        /// Determines the maintenance tax required for all buildings at their current levels.
        pub fn base_maintenance(&self, stats: &InfrastructureStats) -> Result<Maintenance> {
          let mut maintenance = Maintenance::default();
          $(
            let building = &self.[<$building:snake>];
            if building.level() > 0u8 && building.is_enabled() {
              let building_stats = stats.building(BuildingId::$building)?;
              maintenance += building.maintenance(&building_stats)?;
            }
          )+

          Ok(maintenance)
        }

        /// Determines whether the infrastructure meets the requirements.
        pub fn has_required_levels(&self, requirements: &InfrastructureRequirements) -> bool {
          $(self.[<$building:snake>].level() >= requirements.[<$building:snake>] &&)+ true
        }
      }
    }
  };
}

impl_infrastructure!(
  Academy, Farm, IronMine, Prefecture, Quarry, Sawmill, Silo, Stable, Wall, Warehouse, Workshop
);

macro_rules! impl_recruitment {
  ($building:ident) => {
    paste::paste! {
      impl Infrastructure {
        pub(crate) fn [<add_ $building:snake _recruit_order>](
          &mut self,
          request: &[<$building RecruitOrderRequest>],
          current_resources: Option<&Resources>,
        ) -> Result<&[<$building RecruitOrder>]> {
          self
            .[<$building:snake>]
            .recruit_queue_mut()
            .recruit(request, current_resources)
        }

        #[must_use]
        pub(crate) fn [<cancel_ $building:snake _recruit_order>](
          &mut self,
          id: [<$building RecruitOrderId>]
        ) -> Option<[<$building RecruitOrder>]> {
          self.[<$building:snake>].recruit_queue_mut().cancel(id)
        }

        #[must_use]
        pub(crate) fn [<process_ $building:snake _recruit_queue>](
          &mut self,
          config: &WorldConfig
        ) -> Option<ArmyPersonnel> {
          self
            .[<$building:snake>]
            .process_queue(config)?
            .into_iter()
            .map(Squad::from)
            .collect::<ArmyPersonnel>()
            .pipe(Some)
        }
      }
    }
  };
}

impl_recruitment!(Academy);
impl_recruitment!(Stable);
impl_recruitment!(Workshop);