diff --git a/CHANGELOG.md b/CHANGELOG.md index adbab4c8f9..cf2bf6b0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Made settings less likely to reset when the format changes - Adjusted some keybindings - Consumables can now trigger multiple effects and buffs +- Overhauled overworld spawns depending on chunk attributes ### Removed diff --git a/common/src/generation.rs b/common/src/generation.rs index 65a70e6bf8..d3bb340817 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -8,6 +8,7 @@ pub enum EntityTemplate { Traveller, } +#[derive(Clone)] pub struct EntityInfo { pub pos: Vec3, pub is_waypoint: bool, // Edge case, overrides everything else diff --git a/server/src/cmd.rs b/server/src/cmd.rs index a157dfe00d..88fca9a976 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1561,42 +1561,49 @@ fn handle_debug_column( fn handle_debug_column( server: &mut Server, client: EcsEntity, - _target: EcsEntity, + target: EcsEntity, args: String, action: &ChatCommand, ) { let sim = server.world.sim(); let sampler = server.world.sample_columns(); + let mut wpos = Vec2::new(0, 0); if let Ok((x, y)) = scan_fmt!(&args, &action.arg_fmt(), i32, i32) { - let wpos = Vec2::new(x, y); - /* let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { - e / sz as i32 - }); */ + wpos = Vec2::new(x, y); + } else { + match server.state.read_component_copied::(target) { + Some(pos) => wpos = pos.0.xy().map(|x| x as i32), + None => server.notify_client( + client, + ChatType::CommandError.server_msg(String::from("You have no position.")), + ), + } + } + let msg_generator = || { + let alt = sim.get_interpolated(wpos, |chunk| chunk.alt)?; + let basement = sim.get_interpolated(wpos, |chunk| chunk.basement)?; + let water_alt = sim.get_interpolated(wpos, |chunk| chunk.water_alt)?; + let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?; + let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?; + let humidity = sim.get_interpolated(wpos, |chunk| chunk.humidity)?; + let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?; + let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?; + let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?; + let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); + let chunk = sim.get(chunk_pos)?; + let col = sampler.get((wpos, server.index.as_index_ref()))?; + let gradient = sim.get_gradient_approx(chunk_pos)?; + let downhill = chunk.downhill; + let river = &chunk.river; + let flux = chunk.flux; - let msg_generator = || { - // let sim_chunk = sim.get(chunk_pos)?; - let alt = sim.get_interpolated(wpos, |chunk| chunk.alt)?; - let basement = sim.get_interpolated(wpos, |chunk| chunk.basement)?; - let water_alt = sim.get_interpolated(wpos, |chunk| chunk.water_alt)?; - let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?; - let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?; - let humidity = sim.get_interpolated(wpos, |chunk| chunk.humidity)?; - let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?; - let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?; - let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?; - let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); - let chunk = sim.get(chunk_pos)?; - let col = sampler.get((wpos, server.index.as_index_ref()))?; - let downhill = chunk.downhill; - let river = &chunk.river; - let flux = chunk.flux; - - Some(format!( - r#"wpos: {:?} + Some(format!( + r#"wpos: {:?} alt {:?} ({:?}) water_alt {:?} ({:?}) basement {:?} river {:?} +gradient {:?} downhill {:?} chaos {:?} flux {:?} @@ -1605,35 +1612,30 @@ humidity {:?} rockiness {:?} tree_density {:?} spawn_rate {:?} "#, - wpos, - alt, - col.alt, - water_alt, - col.water_level, - basement, - river, - downhill, - chaos, - flux, - temp, - humidity, - rockiness, - tree_density, - spawn_rate - )) - }; - if let Some(s) = msg_generator() { - server.notify_client(client, ChatType::CommandInfo.server_msg(s)); - } else { - server.notify_client( - client, - ChatType::CommandError.server_msg("Not a pregenerated chunk."), - ); - } + wpos, + alt, + col.alt, + water_alt, + col.water_level, + basement, + river, + gradient, + downhill, + chaos, + flux, + temp, + humidity, + rockiness, + tree_density, + spawn_rate + )) + }; + if let Some(s) = msg_generator() { + server.notify_client(client, ChatType::CommandInfo.server_msg(s)); } else { server.notify_client( client, - ChatType::CommandError.server_msg(action.help_string()), + ChatType::CommandError.server_msg("Not a pregenerated chunk."), ); } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 81dc112430..1e68a88dee 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -54,10 +54,11 @@ use common::{ }, outcome::Outcome, recipe::default_recipe_book, + spiral::Spiral2d, state::{State, TimeOfDay}, sync::WorldSyncExt, terrain::TerrainChunkSize, - vol::{ReadVol, RectVolSize}, + vol::RectVolSize, }; use futures_executor::block_on; use metrics::{PhysicsMetrics, ServerMetrics, StateTickMetrics, TickMetrics}; @@ -525,7 +526,16 @@ impl Server { !&self.state.ecs().read_storage::(), ) .join() - .filter(|(_, pos, _)| terrain.get(pos.0.map(|e| e.floor() as i32)).is_err()) + .filter(|(_, pos, _)| { + let chunk_key = terrain.pos_key(pos.0.map(|e| e.floor() as i32)); + // Check not only this chunk, but also all neighbours to avoid immediate + // despawning if the entity walks outside of a valid chunk + // briefly. If the entity isn't even near a loaded chunk then we get + // rid of it. + Spiral2d::new() + .take(9) + .all(|offs| terrain.get_key(chunk_key + offs).is_none()) + }) .map(|(entity, _, _)| entity) .collect::>() }; diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index e7c672d254..b3dc3ead64 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -11,6 +11,7 @@ use common::{ sync::{Uid, UidAllocator, WorldSyncExt}, util::Dir, }; +use rand::prelude::*; use specs::{ saveload::MarkerAllocator, Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, Join, WorldExt, @@ -121,7 +122,15 @@ impl StateExt for State { .create_entity_synced() .with(pos) .with(comp::Vel(Vec3::zero())) - .with(comp::Ori::default()) + .with(comp::Ori(Dir::new( + Vec3::new( + thread_rng().gen_range(-1.0, 1.0), + thread_rng().gen_range(-1.0, 1.0), + 0.0, + ) + .try_normalized() + .unwrap_or_default(), + ))) .with(comp::Collider::Box { radius: body.radius(), z_min: 0.0, diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 466987a82a..dcfc90f88a 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -981,6 +981,8 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { 5.0 }; + let gradient = sim.get_gradient_approx(chunk_pos); + let path = sim.get_nearest_path(wpos); let cave = sim.get_nearest_cave(wpos); @@ -1024,6 +1026,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { spawn_rate, stone_col, water_dist, + gradient, path, cave, snow_cover, @@ -1053,6 +1056,7 @@ pub struct ColumnSample<'a> { pub spawn_rate: f32, pub stone_col: Rgb, pub water_dist: Option, + pub gradient: Option, pub path: Option<(f32, Vec2, Path, Vec2)>, pub cave: Option<(f32, Vec2, Cave, Vec2)>, pub snow_cover: bool, diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 77b9b1e1e0..d2af2780f8 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -1,5 +1,6 @@ pub mod scatter; pub mod tree; +pub mod wildlife; pub use self::{scatter::apply_scatter_to, tree::apply_trees_to}; diff --git a/world/src/layer/wildlife.rs b/world/src/layer/wildlife.rs new file mode 100644 index 0000000000..d5552cfbe1 --- /dev/null +++ b/world/src/layer/wildlife.rs @@ -0,0 +1,717 @@ +use crate::{column::ColumnSample, sim::SimChunk, IndexRef, CONFIG}; +use common::{ + comp::{biped_large, bird_medium, quadruped_low, quadruped_medium, quadruped_small, Alignment}, + generation::{ChunkSupplement, EntityInfo}, + terrain::Block, + vol::{BaseVol, ReadVol, RectSizedVol, WriteVol}, +}; +use rand::prelude::*; +use std::{f32, ops::Range}; +use vek::*; + +fn close(x: f32, tgt: f32, falloff: f32) -> f32 { + (1.0 - (x - tgt).abs() / falloff).max(0.0).powf(0.125) +} + +const BASE_DENSITY: f32 = 1.0e-5; // Base wildlife density + +#[allow(clippy::eval_order_dependence)] +pub fn apply_wildlife_supplement<'a, R: Rng>( + // NOTE: Used only for dynamic elements like chests and entities! + dynamic_rng: &mut R, + wpos2d: Vec2, + mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, + vol: &(impl BaseVol + RectSizedVol + ReadVol + WriteVol), + _index: IndexRef, + chunk: &SimChunk, + supplement: &mut ChunkSupplement, +) { + struct Entry { + make_entity: fn(Vec3, &mut R) -> EntityInfo, // Entity + group_size: Range, // Group size range + is_underwater: bool, // Underwater? + get_density: fn(&SimChunk, &ColumnSample) -> f32, // Density + } + + let scatter: &[Entry] = &[ + // Tundra pack ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 3) { + 0 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Frostfang, + ) + .into(), + 1 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Roshwalr, + ) + .into(), + _ => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Grolgar, + ) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..4, + is_underwater: false, + get_density: |c, _col| close(c.temp, CONFIG.snow_temp, 0.3) * BASE_DENSITY * 1.0, + }, + // Tundra rare solitary ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body( + biped_large::Body::random_with(rng, &biped_large::Species::Wendigo).into(), + ) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.1, + }, + // Taiga pack ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body( + quadruped_medium::Body::random_with(rng, &quadruped_medium::Species::Wolf) + .into(), + ) + .with_alignment(Alignment::Enemy) + }, + group_size: 3..8, + is_underwater: false, + get_density: |c, col| { + close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * col.tree_density * BASE_DENSITY * 1.0 + }, + }, + // Taiga pack wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body( + quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Mouflon, + ) + .into(), + ) + .with_alignment(Alignment::Wild) + }, + group_size: 1..4, + is_underwater: false, + get_density: |c, _col| close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * BASE_DENSITY * 1.0, + }, + // Taiga solitary wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 5) { + 0 => { + bird_medium::Body::random_with(rng, &bird_medium::Species::Eagle).into() + }, + 1 => quadruped_low::Body::random_with(rng, &quadruped_low::Species::Asp) + .into(), + 2 => bird_medium::Body::random_with(rng, &bird_medium::Species::Snowyowl) + .into(), + 3 => quadruped_small::Body { + species: quadruped_small::Species::Fox, + body_type: quadruped_small::BodyType::Female, + } + .into(), + _ => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Tuskram, + ) + .into(), + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * BASE_DENSITY * 5.0, + }, + // Temperate pack ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 2) { + 0 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Tarasque, + ) + .into(), + _ => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Saber, + ) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| close(c.temp, CONFIG.temperate_temp, 0.35) * BASE_DENSITY * 1.0, + }, + // Temperate pack wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 11) { + 0 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Deer, + ) + .into(), + 1 => { + quadruped_small::Body::random_with(rng, &quadruped_small::Species::Rat) + .into() + }, + 2 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Rabbit, + ) + .into(), + 3 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Jackalope, + ) + .into(), + 4 => { + quadruped_small::Body::random_with(rng, &quadruped_small::Species::Boar) + .into() + }, + 5 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Sheep, + ) + .into(), + 6 => { + quadruped_small::Body::random_with(rng, &quadruped_small::Species::Pig) + .into() + }, + 7 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Squirrel, + ) + .into(), + 8 => { + quadruped_small::Body::random_with(rng, &quadruped_small::Species::Hare) + .into() + }, + 9 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Horse, + ) + .into(), + _ => bird_medium::Body::random_with(rng, &bird_medium::Species::Chicken) + .into(), + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..8, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.temperate_temp, 0.5) + * close(c.humidity, CONFIG.forest_hum, 0.4) + //* col.tree_density + * BASE_DENSITY + * 4.0 + }, + }, + // Temperate solitary wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 15) { + 0 => quadruped_small::Body { + species: quadruped_small::Species::Fox, + body_type: quadruped_small::BodyType::Male, + } + .into(), + 1 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Quokka, + ) + .into(), + 2 => { + bird_medium::Body::random_with(rng, &bird_medium::Species::Goose).into() + }, + 3 => bird_medium::Body::random_with(rng, &bird_medium::Species::Peacock) + .into(), + 4 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Porcupine, + ) + .into(), + 5 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Skunk, + ) + .into(), + 6 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Raccoon, + ) + .into(), + 7 => bird_medium::Body::random_with(rng, &bird_medium::Species::Cockatrice) + .into(), + 8 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Catoblepas, + ) + .into(), + 9 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Turtle, + ) + .into(), + 10 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Hirdrasil, + ) + .into(), + 11 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Kelpie, + ) + .into(), + 12 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Truffler, + ) + .into(), + 13 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Donkey, + ) + .into(), + _ => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Batfox, + ) + .into(), + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.temperate_temp, 0.5) + * BASE_DENSITY + * close(c.humidity, CONFIG.forest_hum, 0.4) + * 8.0 + }, + }, + // Rare temperate solitary enemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 4) { + 0 => { + biped_large::Body::random_with(rng, &biped_large::Species::Ogre).into() + }, + 1 => { + biped_large::Body::random_with(rng, &biped_large::Species::Troll).into() + }, + 2 => biped_large::Body::random_with(rng, &biped_large::Species::Dullahan) + .into(), + _ => biped_large::Body::random_with(rng, &biped_large::Species::Cyclops) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| close(c.temp, CONFIG.temperate_temp, 0.8) * BASE_DENSITY * 0.1, + }, + // Temperate river wildlife + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 3) { + 0 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Beaver, + ) + .into(), + 1 => quadruped_low::Body { + species: quadruped_low::Species::Salamander, + body_type: quadruped_low::BodyType::Female, + } + .into(), + _ => { + bird_medium::Body::random_with(rng, &bird_medium::Species::Duck).into() + }, + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..2, + is_underwater: false, + get_density: |_c, col| { + close(col.temp, CONFIG.temperate_temp, 0.6) + * if col.water_dist.map(|d| d < 10.0).unwrap_or(false) { + 0.003 + } else { + 0.0 + } + }, + }, + // Temperate river ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body( + quadruped_low::Body::random_with(rng, &quadruped_low::Species::Hakulaq) + .into(), + ) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |_c, col| { + close(col.temp, CONFIG.temperate_temp, 0.6) + * if col.water_dist.map(|d| d < 10.0).unwrap_or(false) { + 0.0001 + } else { + 0.0 + } + }, + }, + // Tropical rock solitary ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body( + quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Dodarock, + ) + .into(), + ) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, col| { + close(c.temp, CONFIG.tropical_temp + 0.1, 0.5) * col.rock * BASE_DENSITY * 5.0 + }, + }, + // Jungle solitary ennemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 3) { + 0 => { + quadruped_low::Body::random_with(rng, &quadruped_low::Species::Maneater) + .into() + }, + 1 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Tiger, + ) + .into(), + _ => quadruped_low::Body::random_with( + rng, + &quadruped_low::Species::Rocksnapper, + ) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.tropical_temp + 0.1, 0.4) + * close(c.humidity, CONFIG.jungle_hum, 0.3) + * BASE_DENSITY + * 4.0 + }, + }, + // Jungle solitary wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 3) { + 0 => bird_medium::Body::random_with(rng, &bird_medium::Species::Parrot) + .into(), + 1 => { + quadruped_low::Body::random_with(rng, &quadruped_low::Species::Monitor) + .into() + }, + _ => { + quadruped_low::Body::random_with(rng, &quadruped_low::Species::Tortoise) + .into() + }, + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.tropical_temp, 0.5) + * close(c.humidity, CONFIG.jungle_hum, 0.3) + * BASE_DENSITY + * 8.0 + }, + }, + // Tropical rare river enemy + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 2) { + // WE GROW 'EM BIG 'ERE + 0 => quadruped_low::Body::random_with( + rng, + &quadruped_low::Species::Crocodile, + ) + .into(), + _ => quadruped_low::Body::random_with( + rng, + &quadruped_low::Species::Alligator, + ) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..3, + is_underwater: false, + get_density: |_c, col| { + close(col.temp, CONFIG.tropical_temp + 0.2, 0.5) + * if col.water_dist.map(|d| d < 10.0).unwrap_or(false) { + 0.0002 + } else { + 0.0 + } + }, + }, + // Tropical rare river wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 3) { + 0 => { + quadruped_small::Body::random_with(rng, &quadruped_small::Species::Frog) + .into() + }, + 1 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Axolotl, + ) + .into(), + _ => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Fungome, + ) + .into(), + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..3, + is_underwater: false, + get_density: |_c, col| { + close(col.temp, CONFIG.tropical_temp, 0.5) + * if col.water_dist.map(|d| d < 10.0).unwrap_or(false) { + 0.001 + } else { + 0.0 + } + }, + }, + // Tropical pack enemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 2) { + 0 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Lion, + ) + .into(), + _ => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Hyena, + ) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..3, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.tropical_temp + 0.1, 0.4) + * close(c.humidity, CONFIG.desert_hum, 0.4) + * BASE_DENSITY + * 2.0 + }, + }, + // Desert pack wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body( + quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Antelope, + ) + .into(), + ) + .with_alignment(Alignment::Wild) + }, + group_size: 3..8, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.tropical_temp + 0.1, 0.4) + * close(c.humidity, CONFIG.desert_hum, 0.4) + * BASE_DENSITY + * 1.0 + }, + }, + // Desert solitary enemies + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 2) { + 0 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Bonerattler, + ) + .into(), + _ => quadruped_low::Body::random_with( + rng, + &quadruped_low::Species::Sandshark, + ) + .into(), + }) + .with_alignment(Alignment::Enemy) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.desert_temp + 0.2, 0.3) + * close(c.humidity, CONFIG.desert_hum, 0.5) + * BASE_DENSITY + * 1.5 + }, + }, + // Desert solitary wild + Entry { + make_entity: |pos, rng| { + EntityInfo::at(pos) + .with_body(match rng.gen_range(0, 5) { + 0 => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Holladon, + ) + .into(), + 1 => { + quadruped_low::Body::random_with(rng, &quadruped_low::Species::Pangolin) + .into() + }, + 2 => quadruped_medium::Body::random_with( + rng, + &quadruped_medium::Species::Camel, + ) + .into(), + 3 => quadruped_low::Body { + species: quadruped_low::Species::Salamander, + body_type: quadruped_low::BodyType::Male, + } + .into(), + _ => quadruped_small::Body::random_with( + rng, + &quadruped_small::Species::Gecko, + ) + .into(), + }) + .with_alignment(Alignment::Wild) + }, + group_size: 1..2, + is_underwater: false, + get_density: |c, _col| { + close(c.temp, CONFIG.desert_temp + 0.2, 0.3) * BASE_DENSITY * 5.0 + }, + }, + ]; + + for y in 0..vol.size_xy().y as i32 { + for x in 0..vol.size_xy().x as i32 { + let offs = Vec2::new(x, y); + + let wpos2d = wpos2d + offs; + + // Sample terrain + let col_sample = if let Some(col_sample) = get_column(offs) { + col_sample + } else { + continue; + }; + + let underwater = col_sample.water_level > col_sample.alt; + + let entity_group = scatter.iter().enumerate().find_map( + |( + _i, + Entry { + make_entity, + group_size, + is_underwater, + get_density, + }, + )| { + let density = get_density(chunk, col_sample); + if density > 0.0 + && dynamic_rng.gen::() < density + && underwater == *is_underwater + && col_sample.gradient < Some(1.3) + { + Some((make_entity, group_size.clone())) + } else { + None + } + }, + ); + + if let Some((make_entity, group_size)) = entity_group { + let alt = col_sample.alt as i32; + // Find the intersection between ground and air, if there is one near the + // surface + if let Some(solid_end) = (-4..8) + .find(|z| { + vol.get(Vec3::new(offs.x, offs.y, alt + z)) + .map(|b| b.is_solid()) + .unwrap_or(false) + }) + .and_then(|solid_start| { + (1..8).map(|z| solid_start + z).find(|z| { + vol.get(Vec3::new(offs.x, offs.y, alt + z)) + .map(|b| !b.is_solid()) + .unwrap_or(true) + }) + }) + { + let group_size = dynamic_rng.gen_range(group_size.start, group_size.end); + let entity = make_entity( + Vec3::new(wpos2d.x, wpos2d.y, alt + solid_end).map(|e| e as f32), + dynamic_rng, + ); + for e in 0..group_size { + let mut entity = entity.clone(); + entity.pos = entity.pos.map(|e| e + dynamic_rng.gen::()) + + Vec3::new( + (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).sin(), + (e as f32 / group_size as f32 * 2.0 * f32::consts::PI).cos(), + 0.0, + ); + supplement.add_entity(entity.with_automatic_name()); + } + } + } + } + } +} diff --git a/world/src/lib.rs b/world/src/lib.rs index 5fccc613c9..172c6f3f1d 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -41,7 +41,6 @@ use crate::{ util::{Grid, Sampler}, }; use common::{ - comp::{self, bird_medium, quadruped_low, quadruped_medium, quadruped_small}, generation::{ChunkSupplement, EntityInfo}, msg::WorldMapMsg, terrain::{Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize}, @@ -246,80 +245,8 @@ impl World { (Vec3::from(chunk_wpos2d) + lpos).map(|e: i32| e as f32) + 0.5 }; - const SPAWN_RATE: f32 = 0.1; let mut supplement = ChunkSupplement { - entities: if dynamic_rng.gen::() < SPAWN_RATE - && sim_chunk.chaos < 0.5 - && !sim_chunk.is_underwater() - { - // TODO: REFACTOR: Define specific alignments in a config file instead of here - let is_hostile: bool; - let is_giant = dynamic_rng.gen_range(0, 8) == 0; - let quadmed = comp::Body::QuadrupedMedium(quadruped_medium::Body::random()); // Not all of them are hostile so we have to do the rng here - let quadlow = comp::Body::QuadrupedLow(quadruped_low::Body::random()); // Not all of them are hostile so we have to do the rng here - let entity = EntityInfo::at(gen_entity_pos(&mut dynamic_rng)) - .do_if(is_giant, |e| e.into_giant()) - .with_body(match dynamic_rng.gen_range(0, 5) { - 0 => { - match quadmed { - comp::Body::QuadrupedMedium(quadruped_medium) => { - match quadruped_medium.species { - quadruped_medium::Species::Catoblepas => is_hostile = false, - quadruped_medium::Species::Mouflon => is_hostile = false, - quadruped_medium::Species::Tuskram => is_hostile = false, - quadruped_medium::Species::Deer => is_hostile = false, - quadruped_medium::Species::Hirdrasil => is_hostile = false, - quadruped_medium::Species::Donkey => is_hostile = false, - quadruped_medium::Species::Camel => is_hostile = false, - quadruped_medium::Species::Zebra => is_hostile = false, - quadruped_medium::Species::Antelope => is_hostile = false, - quadruped_medium::Species::Kelpie => is_hostile = false, - quadruped_medium::Species::Horse => is_hostile = false, - _ => is_hostile = true, - } - }, - _ => is_hostile = true, - }; - quadmed - }, - 1 => { - is_hostile = false; - comp::Body::BirdMedium(bird_medium::Body::random()) - }, - 2 => { - match quadlow { - comp::Body::QuadrupedLow(quadruped_low) => { - match quadruped_low.species { - quadruped_low::Species::Crocodile => is_hostile = true, - quadruped_low::Species::Alligator => is_hostile = true, - quadruped_low::Species::Maneater => is_hostile = true, - quadruped_low::Species::Sandshark => is_hostile = true, - quadruped_low::Species::Hakulaq => is_hostile = true, - _ => is_hostile = false, - } - }, - _ => is_hostile = false, - }; - quadlow - }, - _ => { - is_hostile = false; - comp::Body::QuadrupedSmall(quadruped_small::Body::random()) - }, - }) - .with_alignment(if is_hostile { - comp::Alignment::Enemy - } else if is_giant { - comp::Alignment::Npc - } else { - comp::Alignment::Wild - }) - .with_automatic_name(); - - vec![entity] - } else { - Vec::new() - }, + entities: Vec::new(), }; if sim_chunk.contains_waypoint { @@ -336,6 +263,17 @@ impl World { &mut supplement, ); + // Apply layer supplement + layer::wildlife::apply_wildlife_supplement( + &mut dynamic_rng, + chunk_wpos2d, + sample_get, + &chunk, + index, + sim_chunk, + &mut supplement, + ); + // Apply site supplementary information sim_chunk.sites.iter().for_each(|site| { index.sites[*site].apply_supplement(