Restructured code. Started working on travel groups.

master
hheik 2025-05-19 14:41:40 +03:00
parent cef8f23521
commit c4c96d0158
6 changed files with 154 additions and 121 deletions

View File

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

View File

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

115
src/sim/combat.rs Normal file
View File

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

View File

@ -39,7 +39,7 @@ pub struct 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 {
id: header.parse_value::<String>().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<Self, ParseError> {
pub fn parse(header: Tag, tags: &[Tag]) -> Result<Self, ParseError> {
let mut result = Self {
id: header.parse_value().unwrap(),
..Default::default()

View File

@ -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<Self, SliceParseError>;

7
src/sim/types.rs Normal file
View File

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