adventure-sim/src/sim/site.rs

261 lines
8.4 KiB
Rust

use std::{collections::HashMap, fmt::Display, time::Duration};
use bevy::prelude::Reflect;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::prelude::*;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize, Reflect)]
pub struct SiteId(Uuid);
impl SiteId {
pub fn generate() -> Self {
Self(Uuid::new_v4())
}
}
impl Display for SiteId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "site-{}", self.0)
}
}
#[derive(Debug, Serialize, Deserialize, Reflect)]
pub struct Site {
id: SiteId,
accumulated_time: Duration,
pub name: String,
pub definition: SiteDef,
pub position: Coords,
pub areas: Vec<SiteArea>,
}
impl Site {
#[inline]
const fn minimum_tick() -> Duration {
Duration::from_secs(3600)
}
pub fn new(
definition: SiteDef,
name: impl Into<String>,
position: Coords,
areas: Vec<SiteArea>,
) -> Self {
Self {
id: SiteId::generate(),
name: name.into(),
definition,
position,
areas,
accumulated_time: Duration::default(),
}
}
pub fn id(&self) -> SiteId {
self.id
}
fn inner_tick(&mut self) {
// Maps where creatures are going during patrol
let mut patrol_map: HashMap<CreatureId, (usize, usize)> = HashMap::new();
let site_size = self.areas.len();
for (area_index, area) in self.areas.iter_mut().enumerate() {
// Resolve combat per area
if let Some(results) = combat::resolve_combat(&area.population, 600) {
for attack in results.attacks {
// println!(
// "{attacker} [{attacker_id}] attacks {target} [{target_id}]: {attack}",
// attacker = attack.attacker.map_or("Natural causes", |attacker| &area
// .find_creature(attacker)
// .unwrap()
// .definition
// .name_singular),
// attacker_id = attack
// .attacker
// .map_or("<no id>".into(), |id| id.to_string()),
// target = area
// .find_creature(attack.target)
// .unwrap()
// .definition
// .name_singular,
// target_id = attack.target
// );
if let Some(damage) = attack.damage {
let target = match area
.population
.iter_mut()
.find(|creature| creature.id() == attack.target)
{
Some(target) => target,
None => continue,
};
target.state.damage = target.state.damage.saturating_add(damage);
}
}
for death in results.kills {
area.population
.retain(|creature| creature.id() != death.killed);
}
}
// Resolve patrolling creatures
if site_size > 1 {
for creature in area.population.iter() {
if fastrand::f32() < creature.patrol_chance() {
// Make sure the creature patrols to a different area
let target_index = {
// Remove the current area from the range
let index = fastrand::usize(0..site_size - 1);
if index < area_index {
index
} else {
// Skip the current area
index + 1
}
};
patrol_map.insert(creature.id(), (area_index, target_index));
}
}
}
}
if !patrol_map.is_empty() {
println!("Patrols:");
for (id, (from, to)) in patrol_map.iter() {
println!("\t[{id}] patrols [{from}] -> [{to}]")
}
}
// Apply patrol map
for (creature_id, (from_index, to_index)) in patrol_map {
let creature = self
.areas
.get_mut(from_index)
.expect("Patrol: getting the area where the patrolling creature is")
.remove_creature(creature_id)
.expect("Patrol: finding the creature inside the area");
self.areas
.get_mut(to_index)
.expect("Patrol: getting the area where the creature is moving to")
.population
.push(creature);
}
println!("Tick! {self}");
}
pub fn generate_from_def(
site_def: &SiteDef,
name: impl Into<String>,
position: Coords,
) -> Self {
Self::new(
site_def.clone(),
name,
position,
vec![
SiteArea::default();
fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize)
],
)
}
pub fn populate_randomly(&mut self, creature_defs: &[&CreatureDef]) {
let area_count = self.areas.len();
{
let min_count = area_count;
let max_count = (5 + (area_count - 1) * 3).max(min_count);
for _ in 0..fastrand::usize(min_count..=max_count) {
let area_t = fastrand::f32().powi(2);
let index = (area_t * (area_count - 1) as f32).round() as usize;
let creature = Creature::generate_from_def(
(*fastrand::choice(creature_defs.iter()).unwrap()).clone(),
);
self.areas
.get_mut(index)
.expect("Creature generation index for depth is out of bounds")
.population
.push(creature)
}
}
}
pub fn advance_time(&mut self, time: Duration) {
self.accumulated_time += time;
while self.accumulated_time >= Self::minimum_tick() {
self.inner_tick();
self.accumulated_time -= Self::minimum_tick();
}
}
/// Finds a creature with given id, removes it from the site and returns it with ownership
pub fn remove_creature(&mut self, id: CreatureId) -> Option<Creature> {
for area in self.areas.iter_mut() {
if let Some(creature) = area.remove_creature(id) {
return Some(creature);
}
}
None
}
}
impl Display for Site {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"site \"{name}\" [{id}] [",
name = self.name,
id = self.id
)?;
for (index, area) in self.areas.iter().enumerate() {
write!(f, "\n\tarea {index} [")?;
for creature in area.population.iter() {
write!(f, "\n\t\t{creature}")?;
}
if !area.population.is_empty() {
write!(f, "\n\t")?;
}
write!(f, "]")?;
}
if !self.areas.is_empty() {
writeln!(f)?;
}
write!(f, "]")
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, Reflect)]
pub struct SiteArea {
pub population: Vec<Creature>,
}
impl SiteArea {
pub fn from_creatures(creatures: &[Creature]) -> Self {
Self {
population: creatures.to_vec(),
}
}
pub fn find_creature(&self, id: CreatureId) -> Option<&Creature> {
self.population.iter().find(|creature| creature.id() == id)
}
pub fn find_creature_mut(&mut self, id: CreatureId) -> Option<&mut Creature> {
self.population
.iter_mut()
.find(|creature| creature.id() == id)
}
/// Finds a creature with given id, removes it from the area and returns it with ownership
pub fn remove_creature(&mut self, id: CreatureId) -> Option<Creature> {
for i in 0..self.population.len() {
if self.population[i].id() == id {
return Some(self.population.swap_remove(i));
}
}
None
}
}