Restructured code. Started working on travel groups.
parent
cef8f23521
commit
c4c96d0158
|
|
@ -1,9 +1,7 @@
|
||||||
use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration};
|
use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration};
|
||||||
|
|
||||||
use definitions::parser::DefinitionParser;
|
use sim::{definitions::parser::DefinitionParser, Creature, Site, SiteArea, World};
|
||||||
use sim::{Creature, Site, SiteArea, World};
|
|
||||||
|
|
||||||
pub mod definitions;
|
|
||||||
pub mod sim;
|
pub mod sim;
|
||||||
|
|
||||||
const SAVE_FILE: &str = "world.bin";
|
const SAVE_FILE: &str = "world.bin";
|
||||||
|
|
|
||||||
142
src/sim.rs
142
src/sim.rs
|
|
@ -5,9 +5,14 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use types::WorldCoords;
|
||||||
use uuid::Uuid;
|
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 {
|
pub fn roll_d20() -> i8 {
|
||||||
fastrand::i8(1..=20)
|
fastrand::i8(1..=20)
|
||||||
|
|
@ -76,7 +81,7 @@ impl Site {
|
||||||
let site_size = self.areas.len();
|
let site_size = self.areas.len();
|
||||||
for (area_index, area) in self.areas.iter_mut().enumerate() {
|
for (area_index, area) in self.areas.iter_mut().enumerate() {
|
||||||
// Resolve combat per area
|
// 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 {
|
for attack in results.attacks {
|
||||||
// println!(
|
// println!(
|
||||||
// "{attacker} [{attacker_id}] attacks {target} [{target_id}]: {attack}",
|
// "{attacker} [{attacker_id}] attacks {target} [{target_id}]: {attack}",
|
||||||
|
|
@ -504,116 +509,25 @@ impl CombatState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> {
|
#[derive(Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
|
||||||
let mut participants: HashMap<CreatureId, CombatState> = HashMap::new();
|
pub struct TravelGroup {
|
||||||
for index_1 in 0..combatants.len() {
|
pub creatures: Vec<Creature>,
|
||||||
for index_2 in (index_1 + 1)..combatants.len() {
|
pub position: WorldCoords,
|
||||||
if index_1 == index_2 {
|
}
|
||||||
continue;
|
|
||||||
}
|
impl TravelGroup {
|
||||||
let c1 = &combatants[index_1];
|
pub fn new(creatures: Vec<Creature>, position: WorldCoords) -> Self {
|
||||||
let c2 = &combatants[index_2];
|
Self {
|
||||||
// Simplest friend-or-foe detection
|
creatures,
|
||||||
let is_enemy = c1.definition.id != c2.definition.id;
|
position,
|
||||||
if is_enemy {
|
}
|
||||||
participants
|
}
|
||||||
.entry(c1.id)
|
|
||||||
.or_insert(CombatState::from_creature(c1.clone()));
|
pub fn insert(&mut self, creature: Creature) {
|
||||||
participants
|
self.creatures.push(creature)
|
||||||
.entry(c2.id)
|
}
|
||||||
.or_insert(CombatState::from_creature(c2.clone()));
|
|
||||||
participants.get_mut(&c1.id).unwrap().enemies.insert(c2.id);
|
pub fn remove(&mut self, id: CreatureId) {
|
||||||
participants.get_mut(&c2.id).unwrap().enemies.insert(c1.id);
|
self.creatures.retain(|creature| creature.id != id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if participants.is_empty() {
|
|
||||||
// No enemies in group :)
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time for violence
|
|
||||||
let mut order: Vec<CreatureId> = 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 })
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> {
|
||||||
|
let mut participants: HashMap<CreatureId, CombatState> = 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<CreatureId> = 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 })
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ pub struct SiteDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SiteDef {
|
impl SiteDef {
|
||||||
fn parse(header: Tag, tags: &[Tag]) -> Result<Self, ParseError> {
|
pub fn parse(header: Tag, tags: &[Tag]) -> Result<Self, ParseError> {
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
id: header.parse_value::<String>().unwrap(),
|
id: header.parse_value::<String>().unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -47,7 +47,6 @@ impl SiteDef {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
match tag.name.as_str() {
|
match tag.name.as_str() {
|
||||||
"SIZE" => (result.min_size, result.max_size) = tag.parse_value().unwrap(),
|
"SIZE" => (result.min_size, result.max_size) = tag.parse_value().unwrap(),
|
||||||
"UNDERGROUND" => result.is_underground = true,
|
|
||||||
"NATURAL" => result.is_natural = true,
|
"NATURAL" => result.is_natural = true,
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("Unknown tag '{}' in SITE defs", tag.name);
|
eprintln!("Unknown tag '{}' in SITE defs", tag.name);
|
||||||
|
|
@ -78,7 +77,7 @@ pub struct CreatureDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreatureDef {
|
impl CreatureDef {
|
||||||
fn parse(header: Tag, tags: &[Tag]) -> Result<Self, ParseError> {
|
pub fn parse(header: Tag, tags: &[Tag]) -> Result<Self, ParseError> {
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
id: header.parse_value().unwrap(),
|
id: header.parse_value().unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{borrow::BorrowMut, io::Read, str::FromStr};
|
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 {
|
pub trait SliceParse: Sized {
|
||||||
fn parse(values: &[String]) -> Result<Self, SliceParseError>;
|
fn parse(values: &[String]) -> Result<Self, SliceParseError>;
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue