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]
[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]

View File

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

View File

@ -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<u32>,
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);
}

View File

@ -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::<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);
std::fs::write("world.bin", bincode::serialize(&world).unwrap()).unwrap();
world

View File

@ -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<SiteArea>,
accumulated_time: Duration,
}
@ -40,59 +56,117 @@ impl Site {
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) {
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<CreatureId, (usize, usize)> = 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("<no id>".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<Creature> {
/// 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<Creature> {
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<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)]
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<Uuid>,
pub killed: CreatureId,
pub killer: Option<CreatureId>,
}
#[derive(Clone, Debug)]
@ -299,8 +431,8 @@ impl AttackRoll {
#[derive(Debug)]
pub struct AttackData {
pub target: Uuid,
pub attacker: Option<Uuid>,
pub target: CreatureId,
pub attacker: Option<CreatureId>,
pub attack_roll: AttackRoll,
pub damage: Option<i32>,
pub is_deathblow: bool,
@ -353,7 +485,7 @@ pub struct CombatResults {
struct CombatState {
creature: Creature,
damage: i32,
enemies: HashSet<Uuid>,
enemies: HashSet<CreatureId>,
}
impl CombatState {
@ -373,7 +505,7 @@ impl CombatState {
}
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_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<Combat
}
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.uuid)
.entry(c1.id)
.or_insert(CombatState::from_creature(c1.clone()));
participants
.entry(c2.uuid)
.entry(c2.id)
.or_insert(CombatState::from_creature(c2.clone()));
participants
.get_mut(&c1.uuid)
.unwrap()
.enemies
.insert(c2.uuid);
participants
.get_mut(&c2.uuid)
.unwrap()
.enemies
.insert(c1.uuid);
participants.get_mut(&c1.id).unwrap().enemies.insert(c2.id);
participants.get_mut(&c2.id).unwrap().enemies.insert(c1.id);
}
}
}
@ -409,32 +534,28 @@ pub fn resolve_combat(combatants: &[Creature], max_rounds: u16) -> Option<Combat
}
// 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);
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<Combat
};
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,
armor_class: participants[&target].creature.definition.armor_class,
};
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::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<Combat
if is_deathblow {
kills.push(KillData {
killed: target,
killer: Some(*uuid),
killer: Some(*id),
})
}
let attack_data = AttackData {
attacker: Some(*uuid),
attacker: Some(*id),
target,
attack_roll,
damage,