use crate::{column::ColumnSample, sim::SimChunk, util::close, IndexRef, CONFIG};
use common::{
assets::{self, AssetExt},
calendar::{Calendar, CalendarEvent},
generation::{ChunkSupplement, EntityInfo},
resources::TimeOfDay,
terrain::{BiomeKind, Block},
time::DayPeriod,
vol::{ReadVol, RectSizedVol, WriteVol},
};
use rand::prelude::*;
use serde::Deserialize;
use std::f32;
use vek::*;
type Weight = u32;
type Min = u8;
type Max = u8;
#[derive(Clone, Debug, Deserialize)]
pub struct SpawnEntry {
pub name: String,
pub note: String,
pub rules: Vec<Pack>,
}
impl assets::Asset for SpawnEntry {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
impl SpawnEntry {
pub fn from(asset_specifier: &str) -> Self { Self::load_expect_cloned(asset_specifier) }
pub fn request(
&self,
requested_period: DayPeriod,
calendar: Option<&Calendar>,
is_underwater: bool,
is_ice: bool,
) -> Option<Pack> {
self.rules
.iter()
.find(|pack| {
let time_match = pack
.day_period
.iter()
.any(|period| *period == requested_period);
let calendar_match = if let Some(calendar) = calendar {
pack.calendar_events.as_ref().map_or(true, |events| {
events.iter().any(|event| calendar.is_event(*event))
})
} else {
false
};
let mode_match = match pack.spawn_mode {
SpawnMode::Land => !is_underwater,
SpawnMode::Ice => is_ice,
SpawnMode::Water | SpawnMode::Underwater => is_underwater,
SpawnMode::Air(_) => true,
};
time_match && calendar_match && mode_match
})
.cloned()
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct Pack {
pub groups: Vec<(Weight, (Min, Max, String))>,
pub spawn_mode: SpawnMode,
pub day_period: Vec<DayPeriod>,
#[serde(default)]
pub calendar_events: Option<Vec<CalendarEvent>>, }
#[derive(Copy, Clone, Debug, Deserialize)]
pub enum SpawnMode {
Land,
Ice,
Water,
Underwater,
Air(f32),
}
impl Pack {
pub fn generate(&self, pos: Vec3<f32>, dynamic_rng: &mut impl Rng) -> (EntityInfo, u8) {
let (_, (from, to, entity_asset)) = self
.groups
.choose_weighted(dynamic_rng, |(p, _group)| *p)
.expect("Failed to choose group");
let entity = EntityInfo::at(pos).with_asset_expect(entity_asset, dynamic_rng, None);
let group_size = dynamic_rng.gen_range(*from..=*to);
(entity, group_size)
}
}
pub type DensityFn = fn(&SimChunk, &ColumnSample) -> f32;
pub fn spawn_manifest() -> Vec<(&'static str, DensityFn)> {
const BASE_DENSITY: f32 = 1.0e-5; vec![
("world.wildlife.spawn.tundra.rock", |c, col| {
close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * col.rock_density * 1.0
}),
("world.wildlife.spawn.tundra.core", |c, _col| {
close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5
}),
(
"world.wildlife.spawn.calendar.christmas.tundra.core",
|c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
),
(
"world.wildlife.spawn.calendar.halloween.tundra.core",
|c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 1.0,
),
(
"world.wildlife.spawn.calendar.april_fools.tundra.core",
|c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
),
(
"world.wildlife.spawn.calendar.easter.tundra.core",
|c, _col| close(c.temp, CONFIG.snow_temp, 0.15) * BASE_DENSITY * 0.5,
),
("world.wildlife.spawn.tundra.snow", |c, col| {
close(c.temp, CONFIG.snow_temp, 0.3) * BASE_DENSITY * col.snow_cover as i32 as f32 * 1.0
}),
(
"world.wildlife.spawn.calendar.christmas.tundra.snow",
|c, col| {
close(c.temp, CONFIG.snow_temp, 0.3)
* BASE_DENSITY
* col.snow_cover as i32 as f32
* 1.0
},
),
(
"world.wildlife.spawn.calendar.halloween.tundra.snow",
|c, col| {
close(c.temp, CONFIG.snow_temp, 0.3)
* BASE_DENSITY
* col.snow_cover as i32 as f32
* 1.5
},
),
(
"world.wildlife.spawn.calendar.april_fools.tundra.snow",
|c, col| {
close(c.temp, CONFIG.snow_temp, 0.3)
* BASE_DENSITY
* col.snow_cover as i32 as f32
* 1.0
},
),
(
"world.wildlife.spawn.calendar.easter.tundra.snow",
|c, col| {
close(c.temp, CONFIG.snow_temp, 0.3)
* BASE_DENSITY
* col.snow_cover as i32 as f32
* 1.0
},
),
("world.wildlife.spawn.tundra.forest", |c, col| {
close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4
}),
("world.wildlife.spawn.tundra.river", |c, col| {
close(col.temp, CONFIG.snow_temp, 0.3)
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt > CONFIG.sea_level + 20.0
{
0.001
} else {
0.0
}
}),
(
"world.wildlife.spawn.calendar.christmas.tundra.forest",
|c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
),
(
"world.wildlife.spawn.calendar.halloween.tundra.forest",
|c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 2.0,
),
(
"world.wildlife.spawn.calendar.april_fools.tundra.forest",
|c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
),
(
"world.wildlife.spawn.calendar.easter.tundra.forest",
|c, col| close(c.temp, CONFIG.snow_temp, 0.3) * col.tree_density * BASE_DENSITY * 1.4,
),
("world.wildlife.spawn.taiga.core_forest", |c, col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
}),
(
"world.wildlife.spawn.calendar.christmas.taiga.core_forest",
|c, col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
},
),
(
"world.wildlife.spawn.calendar.halloween.taiga.core",
|c, col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.8
},
),
(
"world.wildlife.spawn.calendar.april_fools.taiga.core",
|c, col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
},
),
(
"world.wildlife.spawn.calendar.easter.taiga.core",
|c, col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * col.tree_density * BASE_DENSITY * 0.4
},
),
("world.wildlife.spawn.taiga.core", |c, _col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.2) * BASE_DENSITY * 1.0
}),
("world.wildlife.spawn.taiga.forest", |c, col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * col.tree_density * BASE_DENSITY * 0.9
}),
("world.wildlife.spawn.taiga.area", |c, _col| {
close(c.temp, CONFIG.snow_temp + 0.2, 0.6) * BASE_DENSITY * 5.0
}),
("world.wildlife.spawn.taiga.water", |c, col| {
close(c.temp, CONFIG.snow_temp, 0.15) * col.tree_density * BASE_DENSITY * 5.0
}),
("world.wildlife.spawn.taiga.river", |c, col| {
close(col.temp, CONFIG.snow_temp + 0.2, 0.6)
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt > CONFIG.sea_level + 20.0
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.temperate.rare", |c, _col| {
close(c.temp, CONFIG.temperate_temp, 0.8) * BASE_DENSITY * 0.08
}),
("world.wildlife.spawn.temperate.plains", |c, _col| {
close(c.temp, CONFIG.temperate_temp, 0.8)
* close(c.tree_density, 0.0, 0.1)
* BASE_DENSITY
* 5.0
}),
("world.wildlife.spawn.temperate.river", |c, col| {
close(col.temp, CONFIG.temperate_temp, 0.6)
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt > CONFIG.sea_level + 20.0
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.temperate.wood", |c, col| {
close(c.temp, CONFIG.temperate_temp + 0.1, 0.5) * col.tree_density * BASE_DENSITY * 5.0
}),
("world.wildlife.spawn.temperate.rainforest", |c, _col| {
close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
* close(c.humidity, CONFIG.forest_hum, 0.6)
* BASE_DENSITY
* 5.0
}),
(
"world.wildlife.spawn.calendar.halloween.temperate.rainforest",
|c, _col| {
close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
* close(c.humidity, CONFIG.forest_hum, 0.6)
* BASE_DENSITY
* 5.0
},
),
(
"world.wildlife.spawn.calendar.april_fools.temperate.rainforest",
|c, _col| {
close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
* close(c.humidity, CONFIG.forest_hum, 0.6)
* BASE_DENSITY
* 4.0
},
),
(
"world.wildlife.spawn.calendar.easter.temperate.rainforest",
|c, _col| {
close(c.temp, CONFIG.temperate_temp + 0.1, 0.6)
* close(c.humidity, CONFIG.forest_hum, 0.6)
* BASE_DENSITY
* 4.0
},
),
("world.wildlife.spawn.temperate.ocean", |_c, col| {
close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& matches!(col.chunk.get_biome(), BiomeKind::Ocean)
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.temperate.beach", |c, col| {
close(col.temp, CONFIG.temperate_temp, 1.0) / 10.0
* if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt < CONFIG.sea_level + 2.0
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.jungle.rainforest", |c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.2, 0.2)
* close(c.humidity, CONFIG.jungle_hum, 0.2)
* BASE_DENSITY
* 2.8
}),
("world.wildlife.spawn.jungle.rainforest_area", |c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
* close(c.humidity, CONFIG.jungle_hum, 0.2)
* BASE_DENSITY
* 8.0
}),
(
"world.wildlife.spawn.calendar.halloween.jungle.area",
|c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
* close(c.humidity, CONFIG.jungle_hum, 0.2)
* BASE_DENSITY
* 10.0
},
),
(
"world.wildlife.spawn.calendar.april_fools.jungle.area",
|c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
* close(c.humidity, CONFIG.jungle_hum, 0.2)
* BASE_DENSITY
* 8.0
},
),
(
"world.wildlife.spawn.calendar.easter.jungle.area",
|c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.2, 0.3)
* close(c.humidity, CONFIG.jungle_hum, 0.2)
* BASE_DENSITY
* 8.0
},
),
("world.wildlife.spawn.tropical.river", |c, col| {
close(col.temp, CONFIG.tropical_temp, 0.5)
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt > CONFIG.sea_level + 20.0
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.tropical.ocean", |_c, col| {
close(col.temp, CONFIG.tropical_temp, 0.1) / 10.0
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& matches!(col.chunk.get_biome(), BiomeKind::Ocean)
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.tropical.beach", |c, col| {
close(col.temp, CONFIG.tropical_temp, 1.0) / 10.0
* if col.water_dist.map(|d| d < 30.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt < CONFIG.sea_level + 2.0
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.arctic.ocean", |_c, col| {
close(col.temp, CONFIG.snow_temp, 0.25) / 10.0
* if matches!(col.chunk.get_biome(), BiomeKind::Ocean) {
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.tropical.rainforest", |c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
* close(c.humidity, CONFIG.jungle_hum, 0.4)
* BASE_DENSITY
* 2.0
}),
(
"world.wildlife.spawn.calendar.halloween.tropical.rainforest",
|c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
* close(c.humidity, CONFIG.jungle_hum, 0.4)
* BASE_DENSITY
* 3.5
},
),
(
"world.wildlife.spawn.calendar.april_fools.tropical.rainforest",
|c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
* close(c.humidity, CONFIG.jungle_hum, 0.4)
* BASE_DENSITY
* 2.0
},
),
("world.wildlife.spawn.tropical.rock", |c, col| {
close(c.temp, CONFIG.tropical_temp + 0.1, 0.5) * col.rock_density * BASE_DENSITY * 5.0
}),
("world.wildlife.spawn.desert.area", |c, _col| {
close(c.temp, CONFIG.tropical_temp + 0.1, 0.4)
* close(c.humidity, CONFIG.desert_hum, 0.4)
* BASE_DENSITY
* 0.8
}),
("world.wildlife.spawn.desert.wasteland", |c, _col| {
close(c.temp, CONFIG.desert_temp + 0.2, 0.3)
* close(c.humidity, CONFIG.desert_hum, 0.5)
* BASE_DENSITY
* 1.3
}),
("world.wildlife.spawn.desert.river", |c, col| {
close(col.temp, CONFIG.desert_temp + 0.2, 0.3)
* if col.water_dist.map(|d| d < 1.0).unwrap_or(false)
&& !matches!(col.chunk.get_biome(), BiomeKind::Ocean)
&& c.alt > CONFIG.sea_level + 20.0
{
0.001
} else {
0.0
}
}),
("world.wildlife.spawn.desert.hot", |c, _col| {
close(c.temp, CONFIG.desert_temp + 0.2, 0.3) * BASE_DENSITY * 3.8
}),
("world.wildlife.spawn.desert.rock", |c, col| {
close(c.temp, CONFIG.desert_temp + 0.2, 0.05) * col.rock_density * BASE_DENSITY * 4.0
}),
]
}
pub fn apply_wildlife_supplement<'a, R: Rng>(
dynamic_rng: &mut R,
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &(impl RectSizedVol<Vox = Block> + ReadVol + WriteVol),
index: IndexRef,
chunk: &SimChunk,
supplement: &mut ChunkSupplement,
time: Option<&(TimeOfDay, Calendar)>,
) {
let scatter = &index.wildlife_spawns;
let wildlife_density_modifier = index.features.wildlife_density;
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;
let col_sample = if let Some(col_sample) = get_column(offs) {
col_sample
} else {
continue;
};
let is_underwater = col_sample.water_level > col_sample.alt;
let is_ice = col_sample.ice_depth > 0.5 && is_underwater;
let (current_day_period, calendar) = if let Some((time, calendar)) = time {
(DayPeriod::from(time.0), Some(calendar))
} else {
(DayPeriod::Noon, None)
};
let entity_group = scatter
.iter()
.filter_map(|(entry, get_density)| {
let density = get_density(chunk, col_sample) * wildlife_density_modifier;
(density > 0.0)
.then(|| {
entry
.read()
.request(current_day_period, calendar, is_underwater, is_ice)
.and_then(|pack| {
(dynamic_rng.gen::<f32>() < density * col_sample.spawn_rate
&& col_sample.gradient < Some(1.3))
.then_some(pack)
})
})
.flatten()
})
.collect::<Vec<_>>() .choose(dynamic_rng)
.cloned();
if let Some(pack) = entity_group {
let desired_alt = match pack.spawn_mode {
SpawnMode::Land | SpawnMode::Underwater => col_sample.alt,
SpawnMode::Ice => col_sample.water_level + 1.0 + col_sample.ice_depth,
SpawnMode::Water => dynamic_rng.gen_range(
col_sample.alt..col_sample.water_level.max(col_sample.alt + 0.1),
),
SpawnMode::Air(height) => {
col_sample.alt.max(col_sample.water_level)
+ dynamic_rng.gen::<f32>() * height
},
};
let (entity, group_size) = pack.generate(
(wpos2d.map(|e| e as f32) + 0.5).with_z(desired_alt),
dynamic_rng,
);
for e in 0..group_size {
let offs_wpos2d = (Vec2::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(),
) * (5.0 + dynamic_rng.gen::<f32>().powf(0.5) * 5.0))
.map(|e| e as i32);
let offs_wpos2d = (offs + offs_wpos2d)
.clamped(Vec2::zero(), vol.size_xy().map(|e| e as i32) - 1)
- offs;
let z_offset = (0..16)
.map(|z| if z % 2 == 0 { z } else { -z } / 2)
.find(|z| {
(0..2).all(|z2| {
vol.get(
Vec3::new(offs.x, offs.y, desired_alt as i32)
+ offs_wpos2d.with_z(z + z2),
)
.map(|b| !b.is_solid())
.unwrap_or(true)
})
});
if let Some(z_offset) = z_offset {
let mut entity = entity.clone();
entity.pos += offs_wpos2d.with_z(z_offset).map(|e| e as f32);
supplement.add_entity(entity);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hashbrown::HashMap;
#[test]
fn test_load_entries() {
let scatter = spawn_manifest();
for (entry, _) in scatter.into_iter() {
drop(SpawnEntry::from(entry));
}
}
#[test]
fn test_name_uniqueness() {
let scatter = spawn_manifest();
let mut names = HashMap::new();
for (entry, _) in scatter.into_iter() {
let SpawnEntry { name, .. } = SpawnEntry::from(entry);
if let Some(old_entry) = names.insert(name, entry) {
panic!("{}: Found name duplicate with {}", entry, old_entry);
}
}
}
#[test]
fn test_load_entities() {
let scatter = spawn_manifest();
for (entry, _) in scatter.into_iter() {
let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
for pack in rules {
let Pack { groups, .. } = pack;
for group in &groups {
println!("{}:", entry);
let (_, (_, _, asset)) = group;
let dummy_pos = Vec3::new(0.0, 0.0, 0.0);
let mut dummy_rng = thread_rng();
let entity =
EntityInfo::at(dummy_pos).with_asset_expect(asset, &mut dummy_rng, None);
drop(entity);
}
}
}
}
#[test]
fn test_group_choose() {
let scatter = spawn_manifest();
for (entry, _) in scatter.into_iter() {
let SpawnEntry { rules, .. } = SpawnEntry::from(entry);
for pack in rules {
let Pack { groups, .. } = pack;
let dynamic_rng = &mut thread_rng();
let _ = groups
.choose_weighted(dynamic_rng, |(p, _group)| *p)
.unwrap_or_else(|err| {
panic!("{}: Failed to choose random group. Err: {}", entry, err)
});
}
}
}
}