diff --git a/src/main.rs b/src/main.rs index 802920f..5d1cff4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration}; -use definitions::parser::DefinitionParser; -use sim::{Creature, Site, SiteArea, World}; +use sim::{definitions::parser::DefinitionParser, Creature, Site, SiteArea, World}; -pub mod definitions; pub mod sim; const SAVE_FILE: &str = "world.bin"; diff --git a/src/sim.rs b/src/sim.rs index 41f2550..6990ba8 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -5,9 +5,14 @@ use std::{ }; use serde::{Deserialize, Serialize}; +use types::WorldCoords; use uuid::Uuid; -use crate::definitions::{CreatureDef, SiteDef}; +use definitions::{CreatureDef, SiteDef}; + +pub mod combat; +pub mod definitions; +pub mod types; pub fn roll_d20() -> i8 { fastrand::i8(1..=20) @@ -76,7 +81,7 @@ impl Site { 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) { + if let Some(results) = combat::resolve_combat(&area.population, 600) { for attack in results.attacks { // println!( // "{attacker} [{attacker_id}] attacks {target} [{target_id}]: {attack}", @@ -504,116 +509,25 @@ impl CombatState { } } -pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option { - 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 { - continue; - } - let c1 = &combatants[index_1]; - let c2 = &combatants[index_2]; - // Simplest friend-or-foe detection - let is_enemy = c1.definition.id != c2.definition.id; - if is_enemy { - participants - .entry(c1.id) - .or_insert(CombatState::from_creature(c1.clone())); - participants - .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); - } - } - } - - if participants.is_empty() { - // No enemies in group :) - return None; - } - - // Time for violence - 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(|id| participants[id].is_alive()) - }); - if !should_continue { - break; - } - - for id in order.iter() { - for _ in 0..participants[id].creature.definition.attack_count() { - if !participants[id].is_alive() { - continue; - } - let targets: Vec<_> = participants[id] - .enemies - .iter() - .filter(|enemy_id| participants[*enemy_id].is_alive()) - .collect(); - let target = match fastrand::choice(targets.iter()) { - Some(target) => **target, - None => continue, - }; - - let attack = Attack { - to_hit_modifier: participants[id].creature.definition.to_hit_modifier, - // Hard-coded - threat_treshold: 20, - armor_class: participants[&target].creature.definition.armor_class, - }; - - let attack_roll = AttackRoll::roll(attack.clone(), roll_d20); - let mut damage = match attack_roll.result() { - AttackResult::CritMiss | AttackResult::Miss => None, - 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(); - enemy.damage = enemy.damage.saturating_add(damage); - !enemy.is_alive() - } - None => false, - }; - if is_deathblow { - kills.push(KillData { - killed: target, - killer: Some(*id), - }) - } - let attack_data = AttackData { - attacker: Some(*id), - target, - attack_roll, - damage, - is_deathblow, - }; - attacks.push(attack_data); - } - } - } - - Some(CombatResults { kills, attacks }) +#[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/combat.rs b/src/sim/combat.rs new file mode 100644 index 0000000..eaa56e4 --- /dev/null +++ b/src/sim/combat.rs @@ -0,0 +1,115 @@ +use super::*; + +pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option { + 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 { + continue; + } + let c1 = &combatants[index_1]; + let c2 = &combatants[index_2]; + // Simplest friend-or-foe detection + let is_enemy = c1.definition.id != c2.definition.id; + if is_enemy { + participants + .entry(c1.id) + .or_insert(CombatState::from_creature(c1.clone())); + participants + .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); + } + } + } + + if participants.is_empty() { + // No enemies in group :) + return None; + } + + // Time for violence + 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(|id| participants[id].is_alive()) + }); + if !should_continue { + break; + } + + for id in order.iter() { + for _ in 0..participants[id].creature.definition.attack_count() { + if !participants[id].is_alive() { + continue; + } + let targets: Vec<_> = participants[id] + .enemies + .iter() + .filter(|enemy_id| participants[*enemy_id].is_alive()) + .collect(); + let target = match fastrand::choice(targets.iter()) { + Some(target) => **target, + None => continue, + }; + + let attack = Attack { + to_hit_modifier: participants[id].creature.definition.to_hit_modifier, + // Hard-coded + threat_treshold: 20, + armor_class: participants[&target].creature.definition.armor_class, + }; + + let attack_roll = AttackRoll::roll(attack.clone(), roll_d20); + let mut damage = match attack_roll.result() { + AttackResult::CritMiss | AttackResult::Miss => None, + 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(); + enemy.damage = enemy.damage.saturating_add(damage); + !enemy.is_alive() + } + None => false, + }; + if is_deathblow { + kills.push(KillData { + killed: target, + killer: Some(*id), + }) + } + let attack_data = AttackData { + attacker: Some(*id), + target, + attack_roll, + damage, + is_deathblow, + }; + attacks.push(attack_data); + } + } + } + + Some(CombatResults { kills, attacks }) +} diff --git a/src/definitions.rs b/src/sim/definitions.rs similarity index 94% rename from src/definitions.rs rename to src/sim/definitions.rs index c61f49b..49674c8 100644 --- a/src/definitions.rs +++ b/src/sim/definitions.rs @@ -39,7 +39,7 @@ pub struct SiteDef { } impl SiteDef { - fn parse(header: Tag, tags: &[Tag]) -> Result { + pub fn parse(header: Tag, tags: &[Tag]) -> Result { let mut result = Self { id: header.parse_value::().unwrap(), ..Default::default() @@ -47,7 +47,6 @@ impl SiteDef { for tag in tags { 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); @@ -78,7 +77,7 @@ pub struct CreatureDef { } impl CreatureDef { - fn parse(header: Tag, tags: &[Tag]) -> Result { + pub fn parse(header: Tag, tags: &[Tag]) -> Result { let mut result = Self { id: header.parse_value().unwrap(), ..Default::default() diff --git a/src/definitions/parser.rs b/src/sim/definitions/parser.rs similarity index 98% rename from src/definitions/parser.rs rename to src/sim/definitions/parser.rs index 1b86d4e..6cadd16 100644 --- a/src/definitions/parser.rs +++ b/src/sim/definitions/parser.rs @@ -1,6 +1,6 @@ use std::{borrow::BorrowMut, io::Read, str::FromStr}; -use crate::definitions::{CreatureDef, SiteDef, Tag, TopLevelTag}; +use super::{CreatureDef, SiteDef, Tag, TopLevelTag}; pub trait SliceParse: Sized { fn parse(values: &[String]) -> Result; diff --git a/src/sim/types.rs b/src/sim/types.rs new file mode 100644 index 0000000..ea502b9 --- /dev/null +++ b/src/sim/types.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct WorldCoords { + pub x: i32, + pub y: i32, +}