Implemented the PATROL tag for creatures

master
hheik 2025-05-13 16:40:43 +03:00
parent 23e7ea5f06
commit cef8f23521
5 changed files with 293 additions and 104 deletions

View File

@ -4,6 +4,7 @@
[AC:12] [AC:12]
[ATTACK:2:1:8] [ATTACK:2:1:8]
[HUMANOID] [HUMANOID]
[INTELLIGENT]
[PATROL] [PATROL]
[CREATURE:SPIDER] [CREATURE:SPIDER]
@ -14,3 +15,18 @@
[MULTIATTACK:2] [MULTIATTACK:2]
[ANIMAL] [ANIMAL]
[PREFER_DARK] [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]

View File

@ -1,3 +1,4 @@
[SITE:CAVE] [SITE:CAVE]
[SIZE:1:3] [SIZE:1:3]
[UNDERGROUND] [UNDERGROUND]
[NATURAL]

View File

@ -35,6 +35,7 @@ pub struct SiteDef {
pub min_size: i32, pub min_size: i32,
pub max_size: i32, pub max_size: i32,
pub is_underground: bool, pub is_underground: bool,
pub is_natural: bool,
} }
impl SiteDef { impl SiteDef {
@ -47,6 +48,7 @@ impl SiteDef {
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, "UNDERGROUND" => result.is_underground = true,
"NATURAL" => result.is_natural = true,
_ => { _ => {
eprintln!("Unknown tag '{}' in SITE defs", tag.name); eprintln!("Unknown tag '{}' in SITE defs", tag.name);
} }
@ -69,8 +71,10 @@ pub struct CreatureDef {
pub multiattack: Option<u32>, pub multiattack: Option<u32>,
pub is_humanoid: bool, pub is_humanoid: bool,
pub is_animal: bool, pub is_animal: bool,
pub is_intelligent: bool,
pub patrols: bool, pub patrols: bool,
pub prefers_dark: bool, pub prefers_dark: bool,
pub invulnerable: bool,
} }
impl CreatureDef { impl CreatureDef {
@ -91,8 +95,10 @@ impl CreatureDef {
"MULTIATTACK" => result.multiattack = Some(tag.parse_value()?), "MULTIATTACK" => result.multiattack = Some(tag.parse_value()?),
"HUMANOID" => result.is_humanoid = true, "HUMANOID" => result.is_humanoid = true,
"ANIMAL" => result.is_animal = true, "ANIMAL" => result.is_animal = true,
"INTELLIGENT" => result.is_intelligent = true,
"PATROL" => result.patrols = true, "PATROL" => result.patrols = true,
"PREFER_DARK" => result.prefers_dark = true, "PREFER_DARK" => result.prefers_dark = true,
"INVULNERABLE" => result.invulnerable = true,
_ => { _ => {
eprintln!("Unknown tag '{}' in CREATURE defs", tag.name); eprintln!("Unknown tag '{}' in CREATURE defs", tag.name);
} }

View File

@ -1,7 +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 definitions::parser::DefinitionParser;
use sim::{Site, World}; use sim::{Creature, Site, SiteArea, World};
pub mod definitions; pub mod definitions;
pub mod sim; pub mod sim;
@ -44,11 +44,43 @@ fn main() {
let site_def = site_definitions.get("CAVE").unwrap(); let site_def = site_definitions.get("CAVE").unwrap();
let mut world = match std::fs::read(SAVE_FILE) { 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(_) => { Err(_) => {
let mut world = World::default(); 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::<Vec<_>>()); site.populate_randomly(&creature_definitions.values().collect::<Vec<_>>());
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); world.sites.push(site);
std::fs::write("world.bin", bincode::serialize(&world).unwrap()).unwrap(); std::fs::write("world.bin", bincode::serialize(&world).unwrap()).unwrap();
world world

View File

@ -1,7 +1,6 @@
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
fmt::Display, fmt::Display,
num::NonZeroUsize,
time::Duration, 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)] #[derive(Debug, Serialize, Deserialize)]
pub struct Site { pub struct Site {
id: SiteId,
pub name: String, pub name: String,
pub definition: SiteDef,
areas: Vec<SiteArea>, areas: Vec<SiteArea>,
accumulated_time: Duration, accumulated_time: Duration,
} }
@ -40,59 +56,117 @@ impl Site {
Duration::from_secs(3600) 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) { fn inner_tick(&mut self) {
for area in self.areas.iter_mut() { // Maps where creatures are going during patrol
match resolve_combat(&area.population, 600) { let mut patrol_map: HashMap<CreatureId, (usize, usize)> = HashMap::new();
Some(results) => { let site_size = self.areas.len();
for attack in results.attacks { for (area_index, area) in self.areas.iter_mut().enumerate() {
println!( // Resolve combat per area
"{} attacks {} [{}]: {attack}", if let Some(results) = resolve_combat(&area.population, 600) {
attack.attacker.map_or("Natural causes", |attacker| &area for attack in results.attacks {
.population // println!(
.iter() // "{attacker} [{attacker_id}] attacks {target} [{target_id}]: {attack}",
.find(|creat| creat.uuid == attacker) // attacker = attack.attacker.map_or("Natural causes", |attacker| &area
.unwrap() // .find_creature(attacker)
.definition // .unwrap()
.name_singular), // .definition
area.population // .name_singular),
.iter() // attacker_id = attack
.find(|creat| creat.uuid == attack.target) // .attacker
.unwrap() // .map_or("<no id>".into(), |id| id.to_string()),
.definition // target = area
.name_singular, // .find_creature(attack.target)
attack.target // .unwrap()
); // .definition
if let Some(damage) = attack.damage { // .name_singular,
let target = match area // target_id = attack.target
.population // );
.iter_mut() if let Some(damage) = attack.damage {
.find(|creature| creature.uuid == attack.target) let target = match area
{ .population
Some(target) => target, .iter_mut()
None => continue, .find(|creature| creature.id == attack.target)
}; {
target.state.damage = target.state.damage.saturating_add(damage); Some(target) => target,
} None => continue,
} };
for death in results.kills { target.state.damage = target.state.damage.saturating_add(damage);
area.population
.retain(|creature| creature.uuid != death.killed);
} }
} }
None => { for death in results.kills {
// If no combat, there is a chance of migration to other areas 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); println!("Tick! {}", self);
} }
pub fn from_def(site_def: &SiteDef) -> Self { pub fn generate_from_def(site_def: &SiteDef) -> Self {
Self::new( Self::new(
site_def.clone(),
"Gorbo's cave", "Gorbo's cave",
fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize) vec![
.try_into() SiteArea::default();
.expect("Generated site with size of 0"), 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) { for _ in 0..fastrand::usize(min_count..=max_count) {
let area_t = fastrand::f32().powi(2); let area_t = fastrand::f32().powi(2);
let index = (area_t * (area_count - 1) as f32).round() as usize; let index = (area_t * (area_count - 1) as f32).round() as usize;
let creature = let creature = Creature::generate_from_def(
Creature::new((*fastrand::choice(creature_defs.iter()).unwrap()).clone()); (*fastrand::choice(creature_defs.iter()).unwrap()).clone(),
);
self.areas self.areas
.get_mut(index) .get_mut(index)
.expect("Creature generation index for depth is out of bounds") .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) { pub fn advance_time(&mut self, time: Duration) {
self.accumulated_time += time; self.accumulated_time += time;
while self.accumulated_time >= Self::minimum_tick() { 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 /// Finds a creature with given id, removes it from the site and returns it with ownership
pub fn remove_by_uuid(&mut self, creature_uuid: Uuid) -> Option<Creature> { pub fn remove_creature(&mut self, id: CreatureId) -> Option<Creature> {
for area in self.areas.iter_mut() { for area in self.areas.iter_mut() {
for i in 0..area.population.len() { if let Some(creature) = area.remove_creature(id) {
if area.population[i].uuid == creature_uuid { return Some(creature);
return Some(area.population.swap_remove(i));
}
} }
} }
None None
@ -154,7 +219,12 @@ impl Site {
impl Display for Site { impl Display for Site {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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() { for (index, area) in self.areas.iter().enumerate() {
write!(f, "\n\tarea {} [", index)?; write!(f, "\n\tarea {} [", index)?;
for creature in area.population.iter() { for creature in area.population.iter() {
@ -177,27 +247,82 @@ pub struct SiteArea {
pub population: Vec<Creature>, 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)] #[derive(Clone, Default, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
pub struct CreatureState { pub struct CreatureState {
pub damage: i32, 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)] #[derive(Clone, Eq, PartialEq, Hash, Debug, Serialize, Deserialize)]
pub struct Creature { pub struct Creature {
id: CreatureId,
pub definition: CreatureDef, pub definition: CreatureDef,
pub state: CreatureState, pub state: CreatureState,
pub uuid: Uuid,
} }
impl Creature { impl Creature {
pub fn new(definition: CreatureDef) -> Self { pub fn generate_from_def(definition: CreatureDef) -> Self {
Self { Self {
uuid: Uuid::new_v4(), id: CreatureId::generate(),
state: CreatureState::default(), state: CreatureState::default(),
definition, 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 { pub fn health(&self) -> i32 {
self.definition.health - self.state.damage self.definition.health - self.state.damage
} }
@ -209,14 +334,21 @@ impl Creature {
impl Display for Creature { impl Display for Creature {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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)] #[derive(Debug)]
pub struct KillData { pub struct KillData {
pub killed: Uuid, pub killed: CreatureId,
pub killer: Option<Uuid>, pub killer: Option<CreatureId>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -299,8 +431,8 @@ impl AttackRoll {
#[derive(Debug)] #[derive(Debug)]
pub struct AttackData { pub struct AttackData {
pub target: Uuid, pub target: CreatureId,
pub attacker: Option<Uuid>, pub attacker: Option<CreatureId>,
pub attack_roll: AttackRoll, pub attack_roll: AttackRoll,
pub damage: Option<i32>, pub damage: Option<i32>,
pub is_deathblow: bool, pub is_deathblow: bool,
@ -353,7 +485,7 @@ pub struct CombatResults {
struct CombatState { struct CombatState {
creature: Creature, creature: Creature,
damage: i32, damage: i32,
enemies: HashSet<Uuid>, enemies: HashSet<CreatureId>,
} }
impl CombatState { impl CombatState {
@ -373,7 +505,7 @@ impl CombatState {
} }
pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> { pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<CombatResults> {
let mut participants: HashMap<Uuid, CombatState> = HashMap::new(); let mut participants: HashMap<CreatureId, CombatState> = HashMap::new();
for index_1 in 0..combatants.len() { for index_1 in 0..combatants.len() {
for index_2 in (index_1 + 1)..combatants.len() { for index_2 in (index_1 + 1)..combatants.len() {
if index_1 == index_2 { if index_1 == index_2 {
@ -381,24 +513,17 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<Combat
} }
let c1 = &combatants[index_1]; let c1 = &combatants[index_1];
let c2 = &combatants[index_2]; let c2 = &combatants[index_2];
// Simplest friend-or-foe detection
let is_enemy = c1.definition.id != c2.definition.id; let is_enemy = c1.definition.id != c2.definition.id;
if is_enemy { if is_enemy {
participants participants
.entry(c1.uuid) .entry(c1.id)
.or_insert(CombatState::from_creature(c1.clone())); .or_insert(CombatState::from_creature(c1.clone()));
participants participants
.entry(c2.uuid) .entry(c2.id)
.or_insert(CombatState::from_creature(c2.clone())); .or_insert(CombatState::from_creature(c2.clone()));
participants participants.get_mut(&c1.id).unwrap().enemies.insert(c2.id);
.get_mut(&c1.uuid) participants.get_mut(&c2.id).unwrap().enemies.insert(c1.id);
.unwrap()
.enemies
.insert(c2.uuid);
participants
.get_mut(&c2.uuid)
.unwrap()
.enemies
.insert(c1.uuid);
} }
} }
} }
@ -409,32 +534,28 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<Combat
} }
// Time for violence // Time for violence
let mut order: Vec<Uuid> = participants.keys().copied().collect(); let mut order: Vec<CreatureId> = participants.keys().copied().collect();
fastrand::shuffle(&mut order); fastrand::shuffle(&mut order);
let mut kills: Vec<_> = vec![]; let mut kills: Vec<_> = vec![];
let mut attacks: Vec<_> = vec![]; let mut attacks: Vec<_> = vec![];
for _ in 0..max_rounds { for _ in 0..max_rounds {
let should_continue = participants.iter().any(|(_, combat)| { let should_continue = participants.iter().any(|(_, combat)| {
combat.is_alive() combat.is_alive() && combat.enemies.iter().any(|id| participants[id].is_alive())
&& combat
.enemies
.iter()
.any(|uuid| participants[uuid].is_alive())
}); });
if !should_continue { if !should_continue {
break; break;
} }
for uuid in order.iter() { for id in order.iter() {
for _ in 0..participants[uuid].creature.definition.attack_count() { for _ in 0..participants[id].creature.definition.attack_count() {
if !participants[uuid].is_alive() { if !participants[id].is_alive() {
continue; continue;
} }
let targets: Vec<_> = participants[uuid] let targets: Vec<_> = participants[id]
.enemies .enemies
.iter() .iter()
.filter(|enemy_uuid| participants[*enemy_uuid].is_alive()) .filter(|enemy_id| participants[*enemy_id].is_alive())
.collect(); .collect();
let target = match fastrand::choice(targets.iter()) { let target = match fastrand::choice(targets.iter()) {
Some(target) => **target, Some(target) => **target,
@ -442,19 +563,32 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<Combat
}; };
let attack = Attack { let attack = Attack {
to_hit_modifier: participants[uuid].creature.definition.to_hit_modifier, to_hit_modifier: participants[id].creature.definition.to_hit_modifier,
// Hard-coded
threat_treshold: 20, threat_treshold: 20,
armor_class: participants[&target].creature.definition.armor_class, armor_class: participants[&target].creature.definition.armor_class,
}; };
let attack_roll = AttackRoll::roll(attack.clone(), roll_d20); let attack_roll = AttackRoll::roll(attack.clone(), roll_d20);
let damage = match attack_roll.result() { let mut damage = match attack_roll.result() {
AttackResult::CritMiss | AttackResult::Miss => None, AttackResult::CritMiss | AttackResult::Miss => None,
AttackResult::Hit => Some(participants[uuid].creature.definition.damage_roll()), AttackResult::Hit => Some(participants[id].creature.definition.damage_roll()),
AttackResult::Crit => { AttackResult::Crit => Some(
Some(participants[uuid].creature.definition.damage_roll() * 2) 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 { let is_deathblow = match damage {
Some(damage) => { Some(damage) => {
let enemy = participants.get_mut(&target).unwrap(); let enemy = participants.get_mut(&target).unwrap();
@ -466,11 +600,11 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<Combat
if is_deathblow { if is_deathblow {
kills.push(KillData { kills.push(KillData {
killed: target, killed: target,
killer: Some(*uuid), killer: Some(*id),
}) })
} }
let attack_data = AttackData { let attack_data = AttackData {
attacker: Some(*uuid), attacker: Some(*id),
target, target,
attack_roll, attack_roll,
damage, damage,