Split code into more files

master
hheik 2025-05-19 15:06:09 +03:00
parent c4c96d0158
commit 0f0e186c2b
8 changed files with 572 additions and 536 deletions

View File

@ -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;

View File

@ -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<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);
}
}
pub mod site;
pub mod travel_group;
pub mod util;
pub mod world;

View File

@ -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> {
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;
if is_enemy {
participants
.entry(c1.id)
.entry(c1.id())
.or_insert(CombatState::from_creature(c1.clone()));
participants
.entry(c2.id)
.entry(c2.id())
.or_insert(CombatState::from_creature(c2.clone()));
participants.get_mut(&c1.id).unwrap().enemies.insert(c2.id);
participants.get_mut(&c2.id).unwrap().enemies.insert(c1.id);
participants
.get_mut(&c1.id())
.unwrap()
.enemies
.insert(c2.id());
participants
.get_mut(&c2.id())
.unwrap()
.enemies
.insert(c1.id());
}
}
}

76
src/sim/creature.rs Normal file
View File

@ -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
)
}
}

255
src/sim/site.rs Normal file
View File

@ -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
}
}

26
src/sim/travel_group.rs Normal file
View File

@ -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);
}
}

View File

@ -5,3 +5,7 @@ pub struct WorldCoords {
pub x: i32,
pub y: i32,
}
pub fn roll_d20() -> i8 {
fastrand::i8(1..=20)
}

18
src/sim/world.rs Normal file
View File

@ -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);
}
}
}