261 lines
8.4 KiB
Rust
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
|
|
}
|
|
}
|