Added simple world generation for development

site-scene
hheik 2025-07-29 17:26:02 +03:00
parent aad25db759
commit 34e14900eb
11 changed files with 144 additions and 166 deletions

View File

@ -1,7 +1,7 @@
[package] [package]
name = "adventure_sim" name = "adventure_sim"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View File

@ -1,4 +1,12 @@
[SITE:CAVE] [SITE:CAVE]
[SIZE:1:3] [SIZE:1:3]
# [UNDERGROUND] [UNDERGROUND]
[NATURAL] [NATURAL]
[SITE:DUNGEON]
[SIZE:2:3]
[UNDERGROUND]
[SITE:BANDIT_CAMP]
[SIZE:1:1]
# [OUTDOORS]

View File

@ -16,7 +16,7 @@ pub fn init(app: &mut App) {
// .add_systems(Update, systems::demo_2d); // .add_systems(Update, systems::demo_2d);
let definitions = sim::Definitions::from_default_directory().unwrap(); let definitions = sim::Definitions::from_default_directory().unwrap();
let game_world = sim::GameWorld::generate_mock(&definitions); let game_world = sim::GameWorld::generate_random(&definitions);
// let game_world = match std::fs::read(sim::SAVE_FILE) { // let game_world = match std::fs::read(sim::SAVE_FILE) {
// Ok(data) => { // Ok(data) => {
// info!("Reading world data from \"{}\"", sim::SAVE_FILE); // info!("Reading world data from \"{}\"", sim::SAVE_FILE);

View File

@ -5,7 +5,6 @@ pub mod debug;
pub mod game; pub mod game;
pub mod game_setup; pub mod game_setup;
pub mod sim; pub mod sim;
pub mod sim_runner;
pub mod util; pub mod util;
fn main() { fn main() {

View File

@ -47,11 +47,7 @@ impl Creature {
} }
pub fn patrol_chance(&self) -> f32 { pub fn patrol_chance(&self) -> f32 {
if self.definition.patrols { if self.definition.patrols { 0.5 } else { 0.0 }
0.5
} else {
0.0
}
} }
pub fn health(&self) -> i32 { pub fn health(&self) -> i32 {

View File

@ -24,6 +24,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(),
"NATURAL" => result.is_natural = true, "NATURAL" => result.is_natural = true,
"UNDERGROUND" => result.is_underground = true,
_ => { _ => {
// warn!("Unknown tag '{}' in SITE defs", tag.name); // warn!("Unknown tag '{}' in SITE defs", tag.name);
return Err(ParseError::UnknownTag { return Err(ParseError::UnknownTag {

View File

@ -24,10 +24,11 @@ impl Display for SiteId {
#[derive(Debug, Serialize, Deserialize, Reflect)] #[derive(Debug, Serialize, Deserialize, Reflect)]
pub struct Site { pub struct Site {
id: SiteId, id: SiteId,
areas: Vec<SiteArea>,
accumulated_time: Duration, accumulated_time: Duration,
pub name: String, pub name: String,
pub definition: SiteDef, pub definition: SiteDef,
pub position: Coords,
pub areas: Vec<SiteArea>,
} }
impl Site { impl Site {
@ -36,11 +37,17 @@ impl Site {
Duration::from_secs(3600) Duration::from_secs(3600)
} }
pub fn new(definition: SiteDef, name: impl Into<String>, areas: Vec<SiteArea>) -> Self { pub fn new(
definition: SiteDef,
name: impl Into<String>,
position: Coords,
areas: Vec<SiteArea>,
) -> Self {
Self { Self {
id: SiteId::generate(), id: SiteId::generate(),
definition,
name: name.into(), name: name.into(),
definition,
position,
areas, areas,
accumulated_time: Duration::default(), accumulated_time: Duration::default(),
} }
@ -139,10 +146,15 @@ impl Site {
println!("Tick! {self}"); println!("Tick! {self}");
} }
pub fn generate_from_def(site_def: &SiteDef) -> Self { pub fn generate_from_def(
site_def: &SiteDef,
name: impl Into<String>,
position: Coords,
) -> Self {
Self::new( Self::new(
site_def.clone(), site_def.clone(),
"Gorbo's cave", name,
position,
vec![ vec![
SiteArea::default(); SiteArea::default();
fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize) fastrand::usize(site_def.min_size as usize..=site_def.max_size as usize)
@ -150,14 +162,6 @@ impl Site {
) )
} }
pub fn areas(&self) -> &Vec<SiteArea> {
self.areas.as_ref()
}
pub fn areas_mut(&mut self) -> &mut Vec<SiteArea> {
self.areas.as_mut()
}
pub fn populate_randomly(&mut self, creature_defs: &[&CreatureDef]) { pub fn populate_randomly(&mut self, creature_defs: &[&CreatureDef]) {
let area_count = self.areas.len(); let area_count = self.areas.len();
{ {

View File

@ -23,7 +23,7 @@ impl Display for TravelGroupId {
#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Reflect)] #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Reflect)]
pub enum WorldPoint { pub enum WorldPoint {
Coords(WorldCoords), Coords(Coords),
Site(SiteId), Site(SiteId),
} }
@ -41,7 +41,7 @@ pub struct TravelGroup {
id: TravelGroupId, id: TravelGroupId,
accumulated_movement: Kilometer, accumulated_movement: Kilometer,
pub creatures: Vec<Creature>, pub creatures: Vec<Creature>,
pub position: WorldCoords, pub position: Coords,
pub origin: Option<WorldPoint>, pub origin: Option<WorldPoint>,
pub destination: Option<WorldPoint>, pub destination: Option<WorldPoint>,
} }
@ -49,7 +49,7 @@ pub struct TravelGroup {
impl TravelGroup { impl TravelGroup {
pub fn new( pub fn new(
creatures: Vec<Creature>, creatures: Vec<Creature>,
position: WorldCoords, position: Coords,
origin: Option<WorldPoint>, origin: Option<WorldPoint>,
destination: Option<WorldPoint>, destination: Option<WorldPoint>,
) -> Self { ) -> Self {

View File

@ -1,4 +1,4 @@
use std::{fmt::Display, path::PathBuf}; use std::{fmt::Display, ops::Range, path::PathBuf};
use bevy::prelude::Reflect; use bevy::prelude::Reflect;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -6,12 +6,62 @@ use serde::{Deserialize, Serialize};
pub type Kilometer = f32; pub type Kilometer = f32;
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Hash, Reflect)] #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Hash, Reflect)]
pub struct WorldCoords { pub struct Coords {
pub x: i32, pub x: i32,
pub y: i32, pub y: i32,
} }
impl Display for WorldCoords { impl Coords {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Hash, Reflect)]
pub struct CoordRect {
pub origin: Coords,
pub size: Coords,
}
impl CoordRect {
pub fn from_size(center: Coords, size: Coords) -> Self {
Self {
origin: Coords {
x: center.x - size.x / 2,
y: center.y - size.y / 2,
},
size,
}
}
pub fn is_empty(&self) -> bool {
self.size.x <= 0 || self.size.y <= 0
}
pub fn width_range(&self) -> Range<i32> {
self.origin.x..(self.origin.x + self.size.x)
}
pub fn height_range(&self) -> Range<i32> {
self.origin.y..(self.origin.y + self.size.y)
}
/// Return iterator of all possible coordinates in rect, also known as the cartesian product of
/// the x and y ranges
pub fn iter_coords(&self) -> impl Iterator<Item = Coords> {
self.width_range()
.flat_map(|x| self.height_range().map(move |y| Coords::new(x, y)))
}
pub fn pick_random_coords(&self, count: usize) -> Vec<Coords> {
if self.is_empty() {
return vec![];
}
fastrand::choose_multiple(self.iter_coords(), count)
}
}
impl Display for Coords {
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.x, self.y) write!(f, "({}, {})", self.x, self.y)
} }

View File

@ -5,33 +5,34 @@ use serde::{Deserialize, Serialize};
use super::prelude::*; use super::prelude::*;
#[derive(Debug, Default, Resource, Serialize, Deserialize, Reflect)] #[derive(Debug, Resource, Serialize, Deserialize, Reflect)]
#[reflect(Resource)] #[reflect(Resource)]
pub struct GameWorld { pub struct GameWorld {
pub area: CoordRect,
pub sites: Vec<Site>, pub sites: Vec<Site>,
pub travel_groups: Vec<TravelGroup>, pub travel_groups: Vec<TravelGroup>,
} }
impl GameWorld { impl Default for GameWorld {
pub fn advance_time(&mut self, time: Duration) { fn default() -> Self {
for site in self.sites.iter_mut() { Self {
site.advance_time(time); area: CoordRect::from_size(Coords::new(0, 0), Coords::new(16, 16)),
} sites: vec![],
for travel_group in self.travel_groups.iter_mut() { travel_groups: vec![],
travel_group.advance_time(time);
} }
} }
}
impl GameWorld {
pub fn generate_mock(defs: &Definitions) -> Self { pub fn generate_mock(defs: &Definitions) -> Self {
let cave_def = defs.sites.get("CAVE").unwrap(); let cave_def = defs.sites.get("CAVE").unwrap();
let mut site = Site::generate_from_def(cave_def);
site.populate_randomly(&defs.creatures.values().collect::<Vec<_>>());
let bandit_def = defs.creatures.get("BANDIT").unwrap(); let bandit_def = defs.creatures.get("BANDIT").unwrap();
let spider_def = defs.creatures.get("SPIDER").unwrap(); let spider_def = defs.creatures.get("SPIDER").unwrap();
let cave = Site::new( let cave = Site::new(
cave_def.clone(), cave_def.clone(),
"Gorbo's cave", "Gorbo's cave",
Coords::new(0, 0),
vec![ vec![
SiteArea::from_creatures(&vec![ SiteArea::from_creatures(&vec![
Creature::generate_from_def(defs.creatures.get("DRAGON").unwrap().clone()), Creature::generate_from_def(defs.creatures.get("DRAGON").unwrap().clone()),
@ -61,13 +62,58 @@ impl GameWorld {
Creature::generate_from_def(bandit_def.clone()), Creature::generate_from_def(bandit_def.clone()),
Creature::generate_from_def(bandit_def.clone()), Creature::generate_from_def(bandit_def.clone()),
], ],
WorldCoords { x: 0, y: 0 }, Coords { x: 0, y: 0 },
Some(WorldPoint::Coords(WorldCoords { x: 0, y: 0 })), Some(WorldPoint::Coords(Coords { x: 0, y: 0 })),
Some(WorldPoint::Site(cave.id())), Some(WorldPoint::Site(cave.id())),
); );
Self { Self {
sites: vec![cave], sites: vec![cave],
travel_groups: vec![group], travel_groups: vec![group],
..default()
}
}
pub fn generate_random(defs: &Definitions) -> Self {
let mut world = Self {
area: CoordRect::from_size(Coords::new(0, 0), Coords::new(32, 32)),
..default()
};
const SITE_COUNT: usize = 30;
let site_locations = world.area.pick_random_coords(SITE_COUNT);
let site_locations = site_locations.chunks(SITE_COUNT / 3);
for (chunk, locations) in site_locations.enumerate() {
for (i, coords) in locations.iter().enumerate() {
world.sites.push(match chunk {
0 => Site::generate_from_def(
defs.sites.get("DUNGEON").unwrap(),
format!("Dungeon #{}", i + 1),
*coords,
),
1 => Site::generate_from_def(
defs.sites.get("CAVE").unwrap(),
format!("Cave #{}", i + 1),
*coords,
),
_ => Site::generate_from_def(
defs.sites.get("BANDIT_CAMP").unwrap(),
format!("Bandit camp #{}", i + 1),
*coords,
),
})
}
}
world
}
pub fn advance_time(&mut self, time: Duration) {
for site in self.sites.iter_mut() {
site.advance_time(time);
}
for travel_group in self.travel_groups.iter_mut() {
travel_group.advance_time(time);
} }
} }
} }

View File

@ -1,126 +0,0 @@
use crate::sim;
use std::{collections::HashMap, io::BufReader, path::PathBuf, time::Duration};
use sim::definitions::parser::DefinitionParser;
use sim::prelude::*;
const SAVE_FILE: &str = "world.bin";
pub fn run_interactive_simulation() {
let mut parse_error_files = vec![];
let mut site_definitions = HashMap::new();
let mut creature_definitions = HashMap::new();
for entry in std::fs::read_dir(resources_path()).unwrap().flatten() {
if entry.file_name().to_string_lossy().ends_with(".def") {
let source = BufReader::new(std::fs::File::open(entry.path()).unwrap());
match DefinitionParser::parse(source) {
Ok(defs) => {
for def in defs.0 {
if let Some(prev) = site_definitions.insert(def.id.clone(), def) {
eprintln!("Duplicate site definition '{}'", prev.id);
}
}
for def in defs.1 {
if let Some(prev) = creature_definitions.insert(def.id.clone(), def) {
eprintln!("Duplicate site definition '{}'", prev.id);
}
}
}
Err(err) => {
parse_error_files.push((err, entry.path()));
}
}
}
}
for (err, path) in parse_error_files.iter() {
eprintln!("Error\t{path:?}\n\t{err:?}")
}
if !parse_error_files.is_empty() {
eprintln!("{} file(s) had parsing errors!", parse_error_files.len())
}
let site_def = site_definitions.get("CAVE").unwrap();
let mut world = match std::fs::read(SAVE_FILE) {
Ok(data) => bincode::deserialize(&data).expect("Loading world data from file"),
Err(_) => {
let mut world = GameWorld::default();
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()),
]),
],
);
let group = TravelGroup::new(
vec![
Creature::generate_from_def(bandit_def.clone()),
Creature::generate_from_def(bandit_def.clone()),
Creature::generate_from_def(bandit_def.clone()),
],
WorldCoords { x: 0, y: 0 },
Some(WorldPoint::Coords(WorldCoords { x: 0, y: 0 })),
Some(WorldPoint::Site(site.id())),
);
world.sites.push(site);
world.travel_groups.push(group);
std::fs::write("world.bin", bincode::serialize(&world).unwrap()).unwrap();
world
}
};
for site in world.sites.iter() {
println!("{site}");
}
for group in world.travel_groups.iter() {
println!("{group}");
}
let mut input = String::new();
loop {
std::io::stdin().read_line(&mut input).unwrap();
if ["q", "quit", "exit"]
.iter()
.any(|quit_cmd| input.trim() == *quit_cmd)
{
break;
}
let start = std::time::Instant::now();
world.advance_time(Duration::from_secs(3600));
let end = std::time::Instant::now();
println!("World tick: {}us", (end - start).as_micros());
}
}
fn resources_path() -> PathBuf {
PathBuf::from(".").join("defs")
}