diff --git a/world/src/canvas.rs b/world/src/canvas.rs index 3e2af65664..b2dae0a3d1 100644 --- a/world/src/canvas.rs +++ b/world/src/canvas.rs @@ -137,7 +137,7 @@ impl<'a> Canvas<'a> { /// inner `CanvasInfo` such that it may be used independently. pub fn info(&mut self) -> CanvasInfo<'a> { self.info } - pub fn get(&mut self, pos: Vec3) -> Block { + pub fn get(&self, pos: Vec3) -> Block { self.chunk .get(pos - self.wpos()) .ok() @@ -222,6 +222,20 @@ impl<'a> Canvas<'a> { }); } + pub fn find_spawn_pos(&self, wpos: Vec3) -> Option> { + let height = 2; + let search_dist: i32 = 8; + + (1..search_dist * 2 + 1) + .rev() + .map(|z| wpos.z + if z % 2 != 0 { z / 2 } else { -(z / 2) }) + .find(|&z| { + self.get(wpos.xy().with_z(z - 1)).is_solid() + && (0..height).all(|z_offs| self.get(wpos.xy().with_z(z + z_offs)).is_fluid()) + }) + .map(|z| wpos.xy().with_z(z)) + } + pub fn spawn(&mut self, entity: EntityInfo) { self.entities.push(entity); } } diff --git a/world/src/layer/spot.rs b/world/src/layer/spot.rs index 6950f40730..75829aa327 100644 --- a/world/src/layer/spot.rs +++ b/world/src/layer/spot.rs @@ -3,32 +3,45 @@ use crate::{ util::seed_expan, Canvas, }; -use common::{ - comp, - generation::EntityInfo, - terrain::{Block, BlockKind, Structure}, -}; +use common::{generation::EntityInfo, terrain::Structure}; use rand::prelude::*; use rand_chacha::ChaChaRng; +use std::ops::Range; use vek::*; +/// Spots are localised structures that spawn in the world. Conceptually, they +/// fit somewhere between the tree generator and the site generator: an attempt +/// to marry the simplicity of the former with the capability of the latter. +/// They are not globally visible to the game: this means that they do not +/// appear on the map, and cannot interact with rtsim (much). +/// +/// To add a new spot, one must: +/// +/// 1. Add a new variant to the [`Spot`] enum. +/// +/// 2. Add a new entry to [`Spot::generate`] that tells the system where to +/// generate your new spot. +/// +/// 3. Add a new arm to the `match` expression in [`Spot::apply_spots_to`] that +/// tells the generator how to generate a spot, including the base structure +/// that composes the spot and the entities that should be spawned there. #[derive(Copy, Clone, Debug)] pub enum Spot { - Camp, - BanditCamp, + MerchantCamp, + SaurokCamp, } impl Spot { pub fn generate(world: &mut WorldSim) { Self::generate_spots( - Spot::Camp, + Spot::MerchantCamp, world, 10.0, |g, c| g < 0.25 && !c.near_cliffs() && !c.river.near_water() && !c.path.0.is_way(), false, ); Self::generate_spots( - Spot::BanditCamp, + Spot::SaurokCamp, world, 10.0, |g, c| g < 0.25 && !c.near_cliffs() && !c.river.near_water() && !c.path.0.is_way(), @@ -37,10 +50,16 @@ impl Spot { } fn generate_spots( + // What kind of spot are we generating? spot: Spot, world: &mut WorldSim, - freq: f32, // Per sq km + // How often should this spot appear (per square km, on average)? + freq: f32, + // What tests should we perform to see whether we can spawn the spot here? The two + // parameters are the gradient of the terrain and the [`SimChunk`] of the candidate + // location. mut valid: impl FnMut(f32, &SimChunk) -> bool, + // Should we allow trees to spawn close to the spot? trees: bool, ) { let world_size = world.get_size(); @@ -60,69 +79,84 @@ impl Spot { } } -pub fn apply_spots_to(canvas: &mut Canvas, dynamic_rng: &mut impl Rng) { +pub fn apply_spots_to(canvas: &mut Canvas, _dynamic_rng: &mut impl Rng) { let nearby_spots = canvas.nearby_spots().collect::>(); - for (spot_wpos, spot, seed) in nearby_spots.iter().copied() { + for (spot_wpos2d, spot, seed) in nearby_spots.iter().copied() { let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); - match spot { - Spot::Camp => { - canvas.foreach_col_area( - Aabr { - min: spot_wpos - 8, - max: spot_wpos + 8, - }, - |canvas, wpos2d, col| { - if nearby_spots - .iter() - .any(|(wpos, _, _)| wpos.distance_squared(wpos2d) < 64) - { - for z in -8..32 { - canvas.set( - wpos2d.with_z(col.alt as i32 + z), - Block::new(BlockKind::Misc, Rgb::broadcast(255)), - ); - } - } - }, - ); - }, - Spot::BanditCamp => { - let structures = Structure::load_group("dungeon_entrances.grassland").read(); - let structure = structures.choose(&mut rng).unwrap(); - let origin = spot_wpos.with_z( - canvas - .col_or_gen(spot_wpos) - .map(|c| c.alt as i32) - .unwrap_or(0), - ); - canvas.blit_structure(origin, &structure, seed); - let spawn_radius = 12; - let avg_num = 5.0; + #[derive(Default)] + struct SpotConfig<'a> { + // The manifest containing a list of possible base structures for the spot (one will be + // chosen) + base_structures: Option<&'a str>, + // The maximum distance from the centre of the spot that entities will spawn + entity_radius: f32, + // The entities that should be spawned in the spot, from closest to furthest + // (count_range, spec) + entities: &'a [(Range, &'a str)], + } - canvas.foreach_col_area( - Aabr { - min: spot_wpos - spawn_radius, - max: spot_wpos + spawn_radius, - }, - |canvas, wpos2d, col| { - if dynamic_rng.gen_bool(avg_num / (spawn_radius * 2).pow(2) as f64) { - if let Some(z) = (-8..8).rev().map(|z| col.alt as i32 + z).find(|z| { - canvas.get(wpos2d.with_z(z + 2)).is_fluid() - && canvas.get(wpos2d.with_z(z + 1)).is_fluid() - && canvas.get(wpos2d.with_z(z + 0)).is_solid() - }) { - canvas.spawn( - EntityInfo::at(wpos2d.map(|e| e as f32 + 0.5).with_z(z as f32)) - .with_asset_expect("common.entity.spot.bandit_camp.saurok") - .with_alignment(comp::Alignment::Enemy), - ); - } - } - }, - ); + let spot_config = match spot { + Spot::MerchantCamp => SpotConfig { + base_structures: Some("trees.quirky"), + entity_radius: 6.0, + entities: &[ + (1..3, "common.entity.village.merchant"), + (2..5, "common.entity.village.villager"), + ], }, + Spot::SaurokCamp => SpotConfig { + base_structures: Some("dungeon_entrances.grassland"), + entity_radius: 12.0, + entities: &[(4..6, "common.entity.spot.bandit_camp.saurok")], + }, + }; + + // Blit base structure + if let Some(base_structures) = spot_config.base_structures { + let structures = Structure::load_group(base_structures).read(); + let structure = structures.choose(&mut rng).unwrap(); + let origin = spot_wpos2d.with_z( + canvas + .col_or_gen(spot_wpos2d) + .map(|c| c.alt as i32) + .unwrap_or(0), + ); + canvas.blit_structure(origin, &structure, seed); + } + + // Spawn entities + const PHI: f32 = 1.618; + let dir_offset = rng.gen::(); + let mut i = 0; + for (spawn_count, spec) in spot_config.entities { + let spawn_count = rng.gen_range(spawn_count.clone()); + + for _ in 0..spawn_count { + let dir = Vec2::new( + ((dir_offset + i as f32 * PHI) * std::f32::consts::TAU).sin(), + ((dir_offset + i as f32 * PHI) * std::f32::consts::TAU).cos(), + ); + let dist = i as f32 / spawn_count as f32 * spot_config.entity_radius; + let wpos2d = spot_wpos2d + (dir * dist).map(|e| e.round() as i32); + + let alt = canvas.col_or_gen(wpos2d).map(|c| c.alt as i32).unwrap_or(0); + + if let Some(wpos) = canvas + .area() + .contains_point(wpos2d) + .then(|| canvas.find_spawn_pos(wpos2d.with_z(alt))) + .flatten() + { + canvas.spawn( + EntityInfo::at(wpos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)) + .with_asset_expect(spec), + ); + } + + i += 1; + } } } }