Split code into more files
parent
c4c96d0158
commit
0f0e186c2b
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration};
|
use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use sim::{definitions::parser::DefinitionParser, Creature, Site, SiteArea, World};
|
use sim::definitions::parser::DefinitionParser;
|
||||||
|
use sim::prelude::{Creature, Site, SiteArea, World};
|
||||||
|
|
||||||
pub mod sim;
|
pub mod sim;
|
||||||
|
|
||||||
|
|
|
||||||
544
src/sim.rs
544
src/sim.rs
|
|
@ -1,533 +1,17 @@
|
||||||
use std::{
|
pub mod prelude {
|
||||||
collections::{HashMap, HashSet},
|
pub use super::combat;
|
||||||
fmt::Display,
|
pub use super::creature::*;
|
||||||
time::Duration,
|
pub use super::definitions::*;
|
||||||
};
|
pub use super::site::*;
|
||||||
|
pub use super::travel_group::*;
|
||||||
use serde::{Deserialize, Serialize};
|
pub use super::util::*;
|
||||||
use types::WorldCoords;
|
pub use super::world::*;
|
||||||
use uuid::Uuid;
|
}
|
||||||
|
|
||||||
use definitions::{CreatureDef, SiteDef};
|
|
||||||
|
|
||||||
pub mod combat;
|
pub mod combat;
|
||||||
|
pub mod creature;
|
||||||
pub mod definitions;
|
pub mod definitions;
|
||||||
pub mod types;
|
pub mod site;
|
||||||
|
pub mod travel_group;
|
||||||
pub fn roll_d20() -> i8 {
|
pub mod util;
|
||||||
fastrand::i8(1..=20)
|
pub mod world;
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
||||||
pub struct World {
|
|
||||||
pub sites: Vec<Site>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl World {
|
|
||||||
pub fn advance_time(&mut self, time: Duration) {
|
|
||||||
for site in self.sites.iter_mut() {
|
|
||||||
site.advance_time(time);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
|
||||||
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, "s-{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Site {
|
|
||||||
id: SiteId,
|
|
||||||
pub name: String,
|
|
||||||
pub definition: SiteDef,
|
|
||||||
areas: Vec<SiteArea>,
|
|
||||||
accumulated_time: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Site {
|
|
||||||
#[inline]
|
|
||||||
const fn minimum_tick() -> Duration {
|
|
||||||
Duration::from_secs(3600)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(definition: SiteDef, name: impl Into<String>, areas: Vec<SiteArea>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: SiteId::generate(),
|
|
||||||
definition,
|
|
||||||
name: name.into(),
|
|
||||||
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) -> Self {
|
|
||||||
Self::new(
|
|
||||||
site_def.clone(),
|
|
||||||
"Gorbo's cave",
|
|
||||||
vec![
|
|
||||||
SiteArea::default();
|
|
||||||
fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn areas(&self) -> &Vec<SiteArea> {
|
|
||||||
self.areas.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn areas_mut(&mut self) -> &mut Vec<SiteArea> {
|
|
||||||
self.areas.as_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Default, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct CreatureState {
|
|
||||||
pub damage: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct CreatureId(Uuid);
|
|
||||||
|
|
||||||
impl CreatureId {
|
|
||||||
pub fn generate() -> Self {
|
|
||||||
Self(Uuid::new_v4())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CreatureId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "c-{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Creature {
|
|
||||||
id: CreatureId,
|
|
||||||
pub definition: CreatureDef,
|
|
||||||
pub state: CreatureState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Creature {
|
|
||||||
pub fn generate_from_def(definition: CreatureDef) -> Self {
|
|
||||||
Self {
|
|
||||||
id: CreatureId::generate(),
|
|
||||||
state: CreatureState::default(),
|
|
||||||
definition,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> CreatureId {
|
|
||||||
self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn patrol_chance(&self) -> f32 {
|
|
||||||
if self.definition.patrols {
|
|
||||||
0.5
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn health(&self) -> i32 {
|
|
||||||
self.definition.health - self.state.damage
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_alive(&self) -> bool {
|
|
||||||
self.health() > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Creature {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{singular} ({hp}/{max_hp}) [{id}]",
|
|
||||||
singular = self.definition.name_singular,
|
|
||||||
hp = self.health(),
|
|
||||||
max_hp = self.definition.health,
|
|
||||||
id = self.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct KillData {
|
|
||||||
pub killed: CreatureId,
|
|
||||||
pub killer: Option<CreatureId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Attack {
|
|
||||||
pub to_hit_modifier: i8,
|
|
||||||
pub threat_treshold: i8,
|
|
||||||
pub armor_class: i8,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum AttackResult {
|
|
||||||
CritMiss,
|
|
||||||
Miss,
|
|
||||||
Hit,
|
|
||||||
Crit,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct AttackRoll {
|
|
||||||
pub attack: Attack,
|
|
||||||
pub roll: i8,
|
|
||||||
pub threat_roll: Option<i8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttackRoll {
|
|
||||||
pub fn roll(attack: Attack, roll_fn: fn() -> i8) -> Self {
|
|
||||||
let roll = roll_fn();
|
|
||||||
let mut threat_roll = None;
|
|
||||||
if roll >= attack.threat_treshold {
|
|
||||||
threat_roll = Some(roll_fn());
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
attack,
|
|
||||||
roll,
|
|
||||||
threat_roll,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_crit_miss(&self) -> bool {
|
|
||||||
self.roll == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_auto_hit(&self) -> bool {
|
|
||||||
self.roll == 20
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_hit(&self) -> bool {
|
|
||||||
if self.is_crit_miss() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if self.is_auto_hit() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
(self.roll + self.attack.to_hit_modifier) >= self.attack.armor_class
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_crit(&self) -> bool {
|
|
||||||
match self.threat_roll {
|
|
||||||
Some(roll) => (roll + self.attack.to_hit_modifier) >= self.attack.armor_class,
|
|
||||||
None => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn result(&self) -> AttackResult {
|
|
||||||
if self.is_crit_miss() {
|
|
||||||
return AttackResult::CritMiss;
|
|
||||||
}
|
|
||||||
match self.is_hit() {
|
|
||||||
true => {
|
|
||||||
if self.is_crit() {
|
|
||||||
AttackResult::Crit
|
|
||||||
} else {
|
|
||||||
AttackResult::Hit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false => AttackResult::Miss,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AttackData {
|
|
||||||
pub target: CreatureId,
|
|
||||||
pub attacker: Option<CreatureId>,
|
|
||||||
pub attack_roll: AttackRoll,
|
|
||||||
pub damage: Option<i32>,
|
|
||||||
pub is_deathblow: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for AttackData {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if let Some(damage) = self.damage {
|
|
||||||
write!(f, "{damage} damage ")?;
|
|
||||||
}
|
|
||||||
match self.attack_roll.result() {
|
|
||||||
AttackResult::CritMiss => write!(f, "*Critical Miss* ")?,
|
|
||||||
AttackResult::Miss => write!(f, "*Miss* ")?,
|
|
||||||
AttackResult::Crit => write!(f, "!Critical hit! ")?,
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
let mod_symbol = if self.attack_roll.attack.to_hit_modifier >= 0 {
|
|
||||||
'+'
|
|
||||||
} else {
|
|
||||||
'-'
|
|
||||||
};
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
": {total} ({calc_roll} {calc_symbol} {calc_mod}) vs. {ac}",
|
|
||||||
total = self.attack_roll.roll + self.attack_roll.attack.to_hit_modifier,
|
|
||||||
calc_roll = self.attack_roll.roll,
|
|
||||||
calc_symbol = mod_symbol,
|
|
||||||
calc_mod = self.attack_roll.attack.to_hit_modifier.abs(),
|
|
||||||
ac = self.attack_roll.attack.armor_class,
|
|
||||||
)?;
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"{}",
|
|
||||||
if self.is_deathblow {
|
|
||||||
" - Killing blow!"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct CombatResults {
|
|
||||||
pub kills: Vec<KillData>,
|
|
||||||
pub attacks: Vec<AttackData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct CombatState {
|
|
||||||
creature: Creature,
|
|
||||||
damage: i32,
|
|
||||||
enemies: HashSet<CreatureId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CombatState {
|
|
||||||
pub fn is_alive(&self) -> bool {
|
|
||||||
self.damage < self.creature.health()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CombatState {
|
|
||||||
pub fn from_creature(creature: Creature) -> Self {
|
|
||||||
Self {
|
|
||||||
creature,
|
|
||||||
damage: 0,
|
|
||||||
enemies: HashSet::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct TravelGroup {
|
|
||||||
pub creatures: Vec<Creature>,
|
|
||||||
pub position: WorldCoords,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TravelGroup {
|
|
||||||
pub fn new(creatures: Vec<Creature>, position: WorldCoords) -> Self {
|
|
||||||
Self {
|
|
||||||
creatures,
|
|
||||||
position,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert(&mut self, creature: Creature) {
|
|
||||||
self.creatures.push(creature)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove(&mut self, id: CreatureId) {
|
|
||||||
self.creatures.retain(|creature| creature.id != id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,168 @@
|
||||||
use super::*;
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
fmt::Display,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct KillData {
|
||||||
|
pub killed: CreatureId,
|
||||||
|
pub killer: Option<CreatureId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Attack {
|
||||||
|
pub to_hit_modifier: i8,
|
||||||
|
pub threat_treshold: i8,
|
||||||
|
pub armor_class: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AttackResult {
|
||||||
|
CritMiss,
|
||||||
|
Miss,
|
||||||
|
Hit,
|
||||||
|
Crit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AttackRoll {
|
||||||
|
pub attack: Attack,
|
||||||
|
pub roll: i8,
|
||||||
|
pub threat_roll: Option<i8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttackRoll {
|
||||||
|
pub fn roll(attack: Attack, roll_fn: fn() -> i8) -> Self {
|
||||||
|
let roll = roll_fn();
|
||||||
|
let mut threat_roll = None;
|
||||||
|
if roll >= attack.threat_treshold {
|
||||||
|
threat_roll = Some(roll_fn());
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
attack,
|
||||||
|
roll,
|
||||||
|
threat_roll,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_crit_miss(&self) -> bool {
|
||||||
|
self.roll == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_auto_hit(&self) -> bool {
|
||||||
|
self.roll == 20
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_hit(&self) -> bool {
|
||||||
|
if self.is_crit_miss() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if self.is_auto_hit() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
(self.roll + self.attack.to_hit_modifier) >= self.attack.armor_class
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_crit(&self) -> bool {
|
||||||
|
match self.threat_roll {
|
||||||
|
Some(roll) => (roll + self.attack.to_hit_modifier) >= self.attack.armor_class,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn result(&self) -> AttackResult {
|
||||||
|
if self.is_crit_miss() {
|
||||||
|
return AttackResult::CritMiss;
|
||||||
|
}
|
||||||
|
match self.is_hit() {
|
||||||
|
true => {
|
||||||
|
if self.is_crit() {
|
||||||
|
AttackResult::Crit
|
||||||
|
} else {
|
||||||
|
AttackResult::Hit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false => AttackResult::Miss,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AttackData {
|
||||||
|
pub target: CreatureId,
|
||||||
|
pub attacker: Option<CreatureId>,
|
||||||
|
pub attack_roll: AttackRoll,
|
||||||
|
pub damage: Option<i32>,
|
||||||
|
pub is_deathblow: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AttackData {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(damage) = self.damage {
|
||||||
|
write!(f, "{damage} damage ")?;
|
||||||
|
}
|
||||||
|
match self.attack_roll.result() {
|
||||||
|
AttackResult::CritMiss => write!(f, "*Critical Miss* ")?,
|
||||||
|
AttackResult::Miss => write!(f, "*Miss* ")?,
|
||||||
|
AttackResult::Crit => write!(f, "!Critical hit! ")?,
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
let mod_symbol = if self.attack_roll.attack.to_hit_modifier >= 0 {
|
||||||
|
'+'
|
||||||
|
} else {
|
||||||
|
'-'
|
||||||
|
};
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
": {total} ({calc_roll} {calc_symbol} {calc_mod}) vs. {ac}",
|
||||||
|
total = self.attack_roll.roll + self.attack_roll.attack.to_hit_modifier,
|
||||||
|
calc_roll = self.attack_roll.roll,
|
||||||
|
calc_symbol = mod_symbol,
|
||||||
|
calc_mod = self.attack_roll.attack.to_hit_modifier.abs(),
|
||||||
|
ac = self.attack_roll.attack.armor_class,
|
||||||
|
)?;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
if self.is_deathblow {
|
||||||
|
" - Killing blow!"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CombatResults {
|
||||||
|
pub kills: Vec<KillData>,
|
||||||
|
pub attacks: Vec<AttackData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct CombatState {
|
||||||
|
creature: Creature,
|
||||||
|
damage: i32,
|
||||||
|
enemies: HashSet<CreatureId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CombatState {
|
||||||
|
pub fn is_alive(&self) -> bool {
|
||||||
|
self.damage < self.creature.health()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CombatState {
|
||||||
|
pub fn from_creature(creature: Creature) -> Self {
|
||||||
|
Self {
|
||||||
|
creature,
|
||||||
|
damage: 0,
|
||||||
|
enemies: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> {
|
pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> {
|
||||||
let mut participants: HashMap<CreatureId, CombatState> = HashMap::new();
|
let mut participants: HashMap<CreatureId, CombatState> = HashMap::new();
|
||||||
|
|
@ -13,13 +177,21 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<Combat
|
||||||
let is_enemy = c1.definition.id != c2.definition.id;
|
let is_enemy = c1.definition.id != c2.definition.id;
|
||||||
if is_enemy {
|
if is_enemy {
|
||||||
participants
|
participants
|
||||||
.entry(c1.id)
|
.entry(c1.id())
|
||||||
.or_insert(CombatState::from_creature(c1.clone()));
|
.or_insert(CombatState::from_creature(c1.clone()));
|
||||||
participants
|
participants
|
||||||
.entry(c2.id)
|
.entry(c2.id())
|
||||||
.or_insert(CombatState::from_creature(c2.clone()));
|
.or_insert(CombatState::from_creature(c2.clone()));
|
||||||
participants.get_mut(&c1.id).unwrap().enemies.insert(c2.id);
|
participants
|
||||||
participants.get_mut(&c2.id).unwrap().enemies.insert(c1.id);
|
.get_mut(&c1.id())
|
||||||
|
.unwrap()
|
||||||
|
.enemies
|
||||||
|
.insert(c2.id());
|
||||||
|
participants
|
||||||
|
.get_mut(&c2.id())
|
||||||
|
.unwrap()
|
||||||
|
.enemies
|
||||||
|
.insert(c1.id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CreatureState {
|
||||||
|
pub damage: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CreatureId(Uuid);
|
||||||
|
|
||||||
|
impl CreatureId {
|
||||||
|
pub fn generate() -> Self {
|
||||||
|
Self(Uuid::new_v4())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CreatureId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "c-{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Creature {
|
||||||
|
id: CreatureId,
|
||||||
|
pub definition: CreatureDef,
|
||||||
|
pub state: CreatureState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Creature {
|
||||||
|
pub fn generate_from_def(definition: CreatureDef) -> Self {
|
||||||
|
Self {
|
||||||
|
id: CreatureId::generate(),
|
||||||
|
state: CreatureState::default(),
|
||||||
|
definition,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> CreatureId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn patrol_chance(&self) -> f32 {
|
||||||
|
if self.definition.patrols {
|
||||||
|
0.5
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn health(&self) -> i32 {
|
||||||
|
self.definition.health - self.state.damage
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_alive(&self) -> bool {
|
||||||
|
self.health() > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Creature {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{singular} ({hp}/{max_hp}) [{id}]",
|
||||||
|
singular = self.definition.name_singular,
|
||||||
|
hp = self.health(),
|
||||||
|
max_hp = self.definition.health,
|
||||||
|
id = self.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
use std::{collections::HashMap, fmt::Display, time::Duration};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||||
|
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, "s-{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Site {
|
||||||
|
id: SiteId,
|
||||||
|
pub name: String,
|
||||||
|
pub definition: SiteDef,
|
||||||
|
areas: Vec<SiteArea>,
|
||||||
|
accumulated_time: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Site {
|
||||||
|
#[inline]
|
||||||
|
const fn minimum_tick() -> Duration {
|
||||||
|
Duration::from_secs(3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(definition: SiteDef, name: impl Into<String>, areas: Vec<SiteArea>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: SiteId::generate(),
|
||||||
|
definition,
|
||||||
|
name: name.into(),
|
||||||
|
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) -> Self {
|
||||||
|
Self::new(
|
||||||
|
site_def.clone(),
|
||||||
|
"Gorbo's cave",
|
||||||
|
vec![
|
||||||
|
SiteArea::default();
|
||||||
|
fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn areas(&self) -> &Vec<SiteArea> {
|
||||||
|
self.areas.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn areas_mut(&mut self) -> &mut Vec<SiteArea> {
|
||||||
|
self.areas.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TravelGroup {
|
||||||
|
pub creatures: Vec<Creature>,
|
||||||
|
pub position: WorldCoords,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TravelGroup {
|
||||||
|
pub fn new(creatures: Vec<Creature>, position: WorldCoords) -> Self {
|
||||||
|
Self {
|
||||||
|
creatures,
|
||||||
|
position,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, creature: Creature) {
|
||||||
|
self.creatures.push(creature)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, id: CreatureId) {
|
||||||
|
self.creatures.retain(|creature| creature.id() != id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,3 +5,7 @@ pub struct WorldCoords {
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
pub y: i32,
|
pub y: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn roll_d20() -> i8 {
|
||||||
|
fastrand::i8(1..=20)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
pub struct World {
|
||||||
|
pub sites: Vec<Site>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl World {
|
||||||
|
pub fn advance_time(&mut self, time: Duration) {
|
||||||
|
for site in self.sites.iter_mut() {
|
||||||
|
site.advance_time(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue