diff --git a/defs/creatures.def b/defs/creatures.def index 9aabae9..5455ddb 100644 --- a/defs/creatures.def +++ b/defs/creatures.def @@ -4,6 +4,7 @@ [AC:12] [ATTACK:2:1:8] [HUMANOID] + [INTELLIGENT] [PATROL] [CREATURE:SPIDER] @@ -14,3 +15,18 @@ [MULTIATTACK:2] [ANIMAL] [PREFER_DARK] + +[CREATURE:DRAGON] + [NAME:dragon:dragons] + [HEALTH:180] + [AC:17] + [ATTACK:10:10:40] + [INTELLIGENT] + [PATROL] + +[CREATURE:INDESTRUCTIBLE] + [NAME:indestructible:indestructibles] + [HEALTH:1] + [AC:0] + [INVULNERABLE] + [ATTACK:0:0:0] diff --git a/defs/sites.def b/defs/sites.def index e4a3742..e549c05 100644 --- a/defs/sites.def +++ b/defs/sites.def @@ -1,3 +1,4 @@ [SITE:CAVE] [SIZE:1:3] [UNDERGROUND] + [NATURAL] diff --git a/src/definitions.rs b/src/definitions.rs index 487ffcd..c61f49b 100644 --- a/src/definitions.rs +++ b/src/definitions.rs @@ -35,6 +35,7 @@ pub struct SiteDef { pub min_size: i32, pub max_size: i32, pub is_underground: bool, + pub is_natural: bool, } impl SiteDef { @@ -47,6 +48,7 @@ impl SiteDef { match tag.name.as_str() { "SIZE" => (result.min_size, result.max_size) = tag.parse_value().unwrap(), "UNDERGROUND" => result.is_underground = true, + "NATURAL" => result.is_natural = true, _ => { eprintln!("Unknown tag '{}' in SITE defs", tag.name); } @@ -69,8 +71,10 @@ pub struct CreatureDef { pub multiattack: Option, pub is_humanoid: bool, pub is_animal: bool, + pub is_intelligent: bool, pub patrols: bool, pub prefers_dark: bool, + pub invulnerable: bool, } impl CreatureDef { @@ -91,8 +95,10 @@ impl CreatureDef { "MULTIATTACK" => result.multiattack = Some(tag.parse_value()?), "HUMANOID" => result.is_humanoid = true, "ANIMAL" => result.is_animal = true, + "INTELLIGENT" => result.is_intelligent = true, "PATROL" => result.patrols = true, "PREFER_DARK" => result.prefers_dark = true, + "INVULNERABLE" => result.invulnerable = true, _ => { eprintln!("Unknown tag '{}' in CREATURE defs", tag.name); } diff --git a/src/main.rs b/src/main.rs index 4b02d42..802920f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration}; use definitions::parser::DefinitionParser; -use sim::{Site, World}; +use sim::{Creature, Site, SiteArea, World}; pub mod definitions; pub mod sim; @@ -44,11 +44,43 @@ fn main() { let site_def = site_definitions.get("CAVE").unwrap(); let mut world = match std::fs::read(SAVE_FILE) { - Ok(data) => bincode::deserialize(&data).unwrap(), + Ok(data) => bincode::deserialize(&data).expect("Loading world data from file"), Err(_) => { let mut world = World::default(); - let mut site = Site::from_def(site_def); + + let mut site = Site::generate_from_def(site_def); site.populate_randomly(&creature_definitions.values().collect::>()); + + let bandit_def = creature_definitions.get("BANDIT").unwrap(); + let spider_def = creature_definitions.get("SPIDER").unwrap(); + let site = Site::new( + site_def.clone(), + "Gorbo's cave", + vec![ + SiteArea::from_creatures(&vec![ + Creature::generate_from_def( + creature_definitions.get("DRAGON").unwrap().clone(), + ), + Creature::generate_from_def( + creature_definitions.get("INDESTRUCTIBLE").unwrap().clone(), + ), + ]), + SiteArea::from_creatures(&vec![ + Creature::generate_from_def(bandit_def.clone()), + Creature::generate_from_def(spider_def.clone()), + ]), + SiteArea::from_creatures(&vec![ + Creature::generate_from_def(bandit_def.clone()), + Creature::generate_from_def(bandit_def.clone()), + Creature::generate_from_def(bandit_def.clone()), + ]), + SiteArea::from_creatures(&vec![ + Creature::generate_from_def(spider_def.clone()), + Creature::generate_from_def(spider_def.clone()), + ]), + ], + ); + world.sites.push(site); std::fs::write("world.bin", bincode::serialize(&world).unwrap()).unwrap(); world diff --git a/src/sim.rs b/src/sim.rs index 844262d..41f2550 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -1,7 +1,6 @@ use std::{ collections::{HashMap, HashSet}, fmt::Display, - num::NonZeroUsize, time::Duration, }; @@ -27,9 +26,26 @@ impl World { } } +#[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, } @@ -40,59 +56,117 @@ impl Site { 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) { - for area in self.areas.iter_mut() { - match resolve_combat(&area.population, 600) { - Some(results) => { - for attack in results.attacks { - println!( - "{} attacks {} [{}]: {attack}", - attack.attacker.map_or("Natural causes", |attacker| &area - .population - .iter() - .find(|creat| creat.uuid == attacker) - .unwrap() - .definition - .name_singular), - area.population - .iter() - .find(|creat| creat.uuid == attack.target) - .unwrap() - .definition - .name_singular, - attack.target - ); - if let Some(damage) = attack.damage { - let target = match area - .population - .iter_mut() - .find(|creature| creature.uuid == 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.uuid != death.killed); + // 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) = 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); } } - None => { - // If no combat, there is a chance of migration to other areas + 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 from_def(site_def: &SiteDef) -> Self { + pub fn generate_from_def(site_def: &SiteDef) -> Self { Self::new( + site_def.clone(), "Gorbo's cave", - fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize) - .try_into() - .expect("Generated site with size of 0"), + vec![ + SiteArea::default(); + fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize) + ], ) } @@ -112,8 +186,9 @@ impl Site { 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::new((*fastrand::choice(creature_defs.iter()).unwrap()).clone()); + 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") @@ -123,14 +198,6 @@ impl Site { } } - pub fn new(name: &str, size: NonZeroUsize) -> Self { - Self { - name: name.into(), - areas: vec![SiteArea::default(); size.get()], - accumulated_time: Duration::default(), - } - } - pub fn advance_time(&mut self, time: Duration) { self.accumulated_time += time; while self.accumulated_time >= Self::minimum_tick() { @@ -139,13 +206,11 @@ impl Site { } } - /// Finds a creature with given uuid, removes it from the site and returns it with ownership - pub fn remove_by_uuid(&mut self, creature_uuid: Uuid) -> Option { + /// 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() { - for i in 0..area.population.len() { - if area.population[i].uuid == creature_uuid { - return Some(area.population.swap_remove(i)); - } + if let Some(creature) = area.remove_creature(id) { + return Some(creature); } } None @@ -154,7 +219,12 @@ impl Site { impl Display for Site { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "site \"{}\" [", self.name)?; + 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() { @@ -177,27 +247,82 @@ 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, - pub uuid: Uuid, } impl Creature { - pub fn new(definition: CreatureDef) -> Self { + pub fn generate_from_def(definition: CreatureDef) -> Self { Self { - uuid: Uuid::new_v4(), + 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 } @@ -209,14 +334,21 @@ impl Creature { impl Display for Creature { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} [{}]", self.definition.name_singular, self.uuid) + 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: Uuid, - pub killer: Option, + pub killed: CreatureId, + pub killer: Option, } #[derive(Clone, Debug)] @@ -299,8 +431,8 @@ impl AttackRoll { #[derive(Debug)] pub struct AttackData { - pub target: Uuid, - pub attacker: Option, + pub target: CreatureId, + pub attacker: Option, pub attack_roll: AttackRoll, pub damage: Option, pub is_deathblow: bool, @@ -353,7 +485,7 @@ pub struct CombatResults { struct CombatState { creature: Creature, damage: i32, - enemies: HashSet, + enemies: HashSet, } impl CombatState { @@ -373,7 +505,7 @@ impl CombatState { } pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option { - let mut participants: HashMap = HashMap::new(); + let mut participants: HashMap = HashMap::new(); for index_1 in 0..combatants.len() { for index_2 in (index_1 + 1)..combatants.len() { if index_1 == index_2 { @@ -381,24 +513,17 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option Option = participants.keys().copied().collect(); + let mut order: Vec = participants.keys().copied().collect(); fastrand::shuffle(&mut order); let mut kills: Vec<_> = vec![]; let mut attacks: Vec<_> = vec![]; for _ in 0..max_rounds { let should_continue = participants.iter().any(|(_, combat)| { - combat.is_alive() - && combat - .enemies - .iter() - .any(|uuid| participants[uuid].is_alive()) + combat.is_alive() && combat.enemies.iter().any(|id| participants[id].is_alive()) }); if !should_continue { break; } - for uuid in order.iter() { - for _ in 0..participants[uuid].creature.definition.attack_count() { - if !participants[uuid].is_alive() { + for id in order.iter() { + for _ in 0..participants[id].creature.definition.attack_count() { + if !participants[id].is_alive() { continue; } - let targets: Vec<_> = participants[uuid] + let targets: Vec<_> = participants[id] .enemies .iter() - .filter(|enemy_uuid| participants[*enemy_uuid].is_alive()) + .filter(|enemy_id| participants[*enemy_id].is_alive()) .collect(); let target = match fastrand::choice(targets.iter()) { Some(target) => **target, @@ -442,19 +563,32 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option None, - AttackResult::Hit => Some(participants[uuid].creature.definition.damage_roll()), - AttackResult::Crit => { - Some(participants[uuid].creature.definition.damage_roll() * 2) - } + AttackResult::Hit => Some(participants[id].creature.definition.damage_roll()), + AttackResult::Crit => Some( + participants[id].creature.definition.max_damage + + participants[id].creature.definition.damage_roll(), + ), }; + if let Some(damage) = damage.as_mut() { + if participants + .get(&target) + .unwrap() + .creature + .definition + .invulnerable + { + *damage = 0; + } + } let is_deathblow = match damage { Some(damage) => { let enemy = participants.get_mut(&target).unwrap(); @@ -466,11 +600,11 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option