diff --git a/src/main.rs b/src/main.rs index 5d1cff4..baf622d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ 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; diff --git a/src/sim.rs b/src/sim.rs index 6990ba8..5a5ee35 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -1,533 +1,17 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt::Display, - time::Duration, -}; - -use serde::{Deserialize, Serialize}; -use types::WorldCoords; -use uuid::Uuid; - -use definitions::{CreatureDef, SiteDef}; +pub mod prelude { + pub use super::combat; + pub use super::creature::*; + pub use super::definitions::*; + pub use super::site::*; + pub use super::travel_group::*; + pub use super::util::*; + pub use super::world::*; +} pub mod combat; +pub mod creature; pub mod definitions; -pub mod types; - -pub fn roll_d20() -> i8 { - fastrand::i8(1..=20) -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct World { - pub sites: Vec, -} - -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, - accumulated_time: Duration, -} - -impl Site { - #[inline] - const fn minimum_tick() -> Duration { - Duration::from_secs(3600) - } - - pub fn new(definition: SiteDef, name: impl Into, areas: Vec) -> 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 = 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("".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 { - self.areas.as_ref() - } - - pub fn areas_mut(&mut self) -> &mut Vec { - 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 { - 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, -} - -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 { - 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, -} - -#[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, -} - -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, - pub attack_roll: AttackRoll, - pub damage: Option, - 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, - pub attacks: Vec, -} - -#[derive(Debug)] -struct CombatState { - creature: Creature, - damage: i32, - enemies: HashSet, -} - -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, - pub position: WorldCoords, -} - -impl TravelGroup { - pub fn new(creatures: Vec, 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); - } -} +pub mod site; +pub mod travel_group; +pub mod util; +pub mod world; diff --git a/src/sim/combat.rs b/src/sim/combat.rs index eaa56e4..bb76894 100644 --- a/src/sim/combat.rs +++ b/src/sim/combat.rs @@ -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, +} + +#[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, +} + +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, + pub attack_roll: AttackRoll, + pub damage: Option, + 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, + pub attacks: Vec, +} + +#[derive(Debug)] +struct CombatState { + creature: Creature, + damage: i32, + enemies: HashSet, +} + +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 { let mut participants: HashMap = HashMap::new(); @@ -13,13 +177,21 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option 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 + ) + } +} diff --git a/src/sim/site.rs b/src/sim/site.rs new file mode 100644 index 0000000..da7eb2d --- /dev/null +++ b/src/sim/site.rs @@ -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, + accumulated_time: Duration, +} + +impl Site { + #[inline] + const fn minimum_tick() -> Duration { + Duration::from_secs(3600) + } + + pub fn new(definition: SiteDef, name: impl Into, areas: Vec) -> 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 = 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("".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 { + self.areas.as_ref() + } + + pub fn areas_mut(&mut self) -> &mut Vec { + 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 { + 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, +} + +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 { + for i in 0..self.population.len() { + if self.population[i].id() == id { + return Some(self.population.swap_remove(i)); + } + } + None + } +} diff --git a/src/sim/travel_group.rs b/src/sim/travel_group.rs new file mode 100644 index 0000000..4e3926e --- /dev/null +++ b/src/sim/travel_group.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use super::prelude::*; + +#[derive(Eq, PartialEq, Hash, Debug, Serialize, Deserialize)] +pub struct TravelGroup { + pub creatures: Vec, + pub position: WorldCoords, +} + +impl TravelGroup { + pub fn new(creatures: Vec, 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); + } +} diff --git a/src/sim/types.rs b/src/sim/util.rs similarity index 77% rename from src/sim/types.rs rename to src/sim/util.rs index ea502b9..4b4de2d 100644 --- a/src/sim/types.rs +++ b/src/sim/util.rs @@ -5,3 +5,7 @@ pub struct WorldCoords { pub x: i32, pub y: i32, } + +pub fn roll_d20() -> i8 { + fastrand::i8(1..=20) +} diff --git a/src/sim/world.rs b/src/sim/world.rs new file mode 100644 index 0000000..4281c97 --- /dev/null +++ b/src/sim/world.rs @@ -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, +} + +impl World { + pub fn advance_time(&mut self, time: Duration) { + for site in self.sites.iter_mut() { + site.advance_time(time); + } + } +}