// TODO: Logging doesn't seem to work here? Figure out why and fix use { crate::{ math_util::{ cartesian_coordinates, mix_values, random_point_in_sphere, CartesianError, RepeatNum, }, perlin, BiomeStats, BiomeType, }, bevy::{ log::info, math::Vec3A, prelude::Vec2, utils::{default, HashMap}, }, rand::{rngs::StdRng, Rng, SeedableRng}, serde::{Deserialize, Serialize}, std::{ error::Error, f32::consts::{PI, TAU}, fmt::{Debug, Display}, }, }; #[derive(Debug, Clone, Copy)] pub enum WorldGenError { CartesianError(CartesianError), } impl Error for WorldGenError { fn source(&self) -> Option<&(dyn Error + 'static)> { match *self { WorldGenError::CartesianError(ref e) => Some(e), } } fn description(&self) -> &str { "description() is deprecated; use Display" } fn cause(&self) -> Option<&dyn Error> { self.source() } } impl Display for WorldGenError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { WorldGenError::CartesianError(err) => Display::fmt(err, f), } } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum CompassDirection { North, NorthEast, East, SouthEast, South, SouthWest, West, NorthWest, } #[derive(Debug, Serialize)] pub struct World { pub width: u32, pub height: u32, pub seed: u32, pub terrain: Vec>, pub continent_offsets: [Vec2; World::NUM_CONTINENTS as usize], pub continent_sizes: [Vec2; World::NUM_CONTINENTS as usize], #[serde(skip)] pub max_altitude: f32, #[serde(skip)] pub min_altitude: f32, #[serde(skip)] pub max_rainfall: f32, #[serde(skip)] pub min_rainfall: f32, #[serde(skip)] pub max_temperature: f32, #[serde(skip)] pub min_temperature: f32, #[serde(skip)] pub rng: StdRng, } #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct TerrainCell { pub altitude: f32, pub rainfall: f32, pub temperature: f32, pub biome_presences: Vec<(BiomeType, f32)>, } impl World { const ALTITUDE_SPAN: f32 = World::MAX_ALTITUDE - World::MIN_ALTITUDE; const CONTINENT_MAX_SIZE_FACTOR: f32 = 6.0; const CONTINENT_MIN_SIZE_FACTOR: f32 = 2.5; pub(crate) const MAX_ALTITUDE: f32 = 15000.0; pub(crate) const MAX_RAINFALL: f32 = 7500.0; pub(crate) const MAX_TEMPERATURE: f32 = 30.0; pub(crate) const MIN_ALTITUDE: f32 = -15000.0; pub(crate) const MIN_RAINFALL: f32 = 0.0; pub(crate) const MIN_TEMPERATURE: f32 = -35.0; const MOUNTAIN_RANGE_MIX_FACTOR: f32 = 0.075; const MOUNTAIN_RANGE_WIDTH_FACTOR: f32 = 25.0; const NUM_CONTINENTS: u8 = 7; const RAINFALL_DRYNESS_FACTOR: f32 = 0.005; const RAINFALL_DRYNESS_OFFSET: f32 = World::RAINFALL_DRYNESS_FACTOR * World::MAX_RAINFALL; const RAINFALL_SPAN: f32 = World::MAX_RAINFALL - World::MIN_RAINFALL; const TEMPERATURE_ALTITUDE_FACTOR: f32 = 1.0; const TEMPERATURE_SPAN: f32 = World::MAX_TEMPERATURE - World::MIN_TEMPERATURE; const TERRAIN_NOISE_FACTOR_1: f32 = 0.15; const TERRAIN_NOISE_FACTOR_2: f32 = 0.15; const TERRAIN_NOISE_FACTOR_3: f32 = 0.1; const TERRAIN_NOISE_FACTOR_4: f32 = 2.5; pub fn new(width: u32, height: u32, seed: u32) -> World { World { width, height, seed, terrain: vec![ vec![TerrainCell::default(); width.try_into().unwrap()]; height.try_into().unwrap() ], continent_offsets: [default(); World::NUM_CONTINENTS as usize], continent_sizes: [default(); World::NUM_CONTINENTS as usize], max_altitude: World::MIN_ALTITUDE, min_altitude: World::MAX_ALTITUDE, max_rainfall: World::MIN_RAINFALL, min_rainfall: World::MAX_RAINFALL, max_temperature: World::MIN_TEMPERATURE, min_temperature: World::MAX_TEMPERATURE, rng: StdRng::seed_from_u64(seed as u64), } } pub fn generate(&mut self) -> Result<(), WorldGenError> { if let Err(err) = self.generate_altitude() { return Err(WorldGenError::CartesianError(err)); } if let Err(err) = self.generate_rainfall() { return Err(WorldGenError::CartesianError(err)); } if let Err(err) = self.generate_temperature() { return Err(WorldGenError::CartesianError(err)); } self.generate_biomes(); Ok(()) } fn generate_continents(&mut self) { info!("Generating continents"); let width = self.width as f32; let height = self.height as f32; for i in 0..World::NUM_CONTINENTS { info!("{}/{}", i, World::NUM_CONTINENTS); self.continent_offsets[i as usize].x = self .rng .gen_range(width * i as f32 * 2.0 / 5.0..(width * (i as f32 + 2.0) * 2.0 / 5.0)) .repeat(width); self.continent_offsets[i as usize].y = self.rng.gen_range(height * 1.0 / 6.0..height * 5.0 / 6.0); self.continent_sizes[i as usize] = Vec2 { x: self .rng .gen_range(World::CONTINENT_MIN_SIZE_FACTOR..World::CONTINENT_MAX_SIZE_FACTOR), y: self .rng .gen_range(World::CONTINENT_MIN_SIZE_FACTOR..World::CONTINENT_MAX_SIZE_FACTOR), }; } info!("Done generating continents"); } fn continent_modifier(&self, x: usize, y: usize) -> f32 { let x = x as f32; let y = y as f32; let width = self.width as f32; let height = self.height as f32; let mut max_value = 0.0; let beta_factor = f32::sin(PI * y / height); for i in 0..World::NUM_CONTINENTS { let idx = i as usize; let Vec2 { x: offset_x, y: offset_y, } = self.continent_offsets[idx]; let Vec2 { x: continent_width, y: continent_height, } = self.continent_sizes[idx]; let distance_x = f32::min( f32::min(f32::abs(offset_x - x), f32::abs(width + offset_x - x)), f32::abs(offset_x - x - width), ) * beta_factor * continent_width; let distance_y = f32::abs(offset_y - y) * continent_height; let distance = f32::sqrt((distance_x * distance_x) + (distance_y * distance_y)); let value = f32::max(0.0, 1.0 - distance / width); if value > max_value { max_value = value; } } max_value } fn generate_altitude(&mut self) -> Result<(), CartesianError> { info!("Generating altitude"); self.generate_continents(); const RADIUS_1: f32 = 0.5; const RADIUS_2: f32 = 4.0; const RADIUS_3: f32 = 4.0; const RADIUS_4: f32 = 8.0; const RADIUS_5: f32 = 16.0; let offset_1 = World::random_offset_vector(&mut self.rng); let offset_2 = World::random_offset_vector(&mut self.rng); let offset_3 = World::random_offset_vector(&mut self.rng); let offset_4 = World::random_offset_vector(&mut self.rng); let offset_5 = World::random_offset_vector(&mut self.rng); for y in 0..self.terrain.len() { let alpha = (y as f32 / self.height as f32) * PI; for x in 0..self.terrain[y].len() { let beta = (x as f32 / self.width as f32) * TAU; let value_1 = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_1, offset_1)?; let value_2 = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_2, offset_2)?; let value_3 = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_3, offset_3)?; let value_4 = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_4, offset_4)?; let value_5 = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_5, offset_5)?; let value_a = mix_values( self.continent_modifier(x, y), value_3, World::TERRAIN_NOISE_FACTOR_1, ) * mix_values(1.0, value_4, World::TERRAIN_NOISE_FACTOR_2) * mix_values(1.0, value_5, World::TERRAIN_NOISE_FACTOR_3); let value_b = value_a * 0.04 + 0.48; let value_c = mix_values( self.mountain_range_noise_from_random_noise(mix_values( value_1, value_2, World::MOUNTAIN_RANGE_MIX_FACTOR, )), value_3, World::TERRAIN_NOISE_FACTOR_1 * World::TERRAIN_NOISE_FACTOR_4, ) * mix_values( 1.0, value_4, World::TERRAIN_NOISE_FACTOR_2 * World::TERRAIN_NOISE_FACTOR_4, ) * mix_values( 1.0, value_5, World::TERRAIN_NOISE_FACTOR_3 * World::TERRAIN_NOISE_FACTOR_4, ); let mut value_d = mix_values(value_a, value_c, 0.25); value_d = mix_values(value_d, value_c, 0.1); value_d = mix_values(value_d, value_b, 0.1); let altitude = World::calculate_altitude(value_d); self.terrain[y][x].altitude = altitude; if altitude > self.max_altitude { self.max_altitude = altitude; } if altitude < self.min_altitude { self.min_altitude = altitude; } } } info!("Done generating altitude"); Ok(()) } fn random_offset_vector(rng: &mut StdRng) -> Vec3A { random_point_in_sphere(rng, 1000.0) } fn random_noise_from_polar_coordinates( &self, alpha: f32, beta: f32, radius: f32, offset: Vec3A, ) -> Result { let cartesian = cartesian_coordinates(alpha, beta, radius)?; Ok(perlin::get_value( cartesian.x + offset.x, cartesian.y + offset.y, cartesian.z + offset.z, )) } fn mountain_range_noise_from_random_noise(&self, noise: f32) -> f32 { let noise = noise * 2.0 - 1.0; let value_1 = -f32::exp(-f32::powi( noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR + 1.0, 2, )); let value_2 = f32::exp(-f32::powi( noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR - 1.0, 2, )); let value_3 = -f32::exp(-f32::powi( noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR + World::MOUNTAIN_RANGE_WIDTH_FACTOR / 2.0, 2, )); let value_4 = f32::exp(-f32::powi( noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR - World::MOUNTAIN_RANGE_WIDTH_FACTOR / 2.0, 2, )); (value_1 + value_2 + value_3 + value_4 + 1.0) / 2.0 } fn calculate_altitude(raw_altitude: f32) -> f32 { World::MIN_ALTITUDE + (raw_altitude * World::ALTITUDE_SPAN) } fn generate_rainfall(&mut self) -> Result<(), CartesianError> { info!("Generating rainfall"); const RADIUS: f32 = 2.0; let offset = World::random_offset_vector(&mut self.rng); let height = self.terrain.len(); for y in 0..height { info!("Rainfall: {}/{}", y, height); let alpha = (y as f32 / self.height as f32) * PI; let width = self.terrain[y].len(); for x in 0..width { let beta = (x as f32 / self.width as f32) * TAU; let random_noise = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS, offset)?; let latitude_factor = (alpha * 0.9) + (random_noise * 0.2 * PI) - 0.1; let latitude_modifier_1 = (1.5 * f32::sin(latitude_factor)) - 0.5; let latitude_modifier_2 = f32::cos(latitude_factor); let offset_cell_x = (width + x + f32::floor(latitude_modifier_2 * width as f32 / 20.0) as usize) % width; let offset_cell_2_x = (width + x + f32::floor(latitude_modifier_2 * width as f32 / 10.0) as usize) % width; let offset_cell = &self.terrain[y][offset_cell_x]; let offset_altitude = f32::max(0.0, offset_cell.altitude); let offset_cell_2 = &self.terrain[y][offset_cell_2_x]; let offset_altitude_2 = f32::max(0.0, offset_cell_2.altitude); let cell = &mut self.terrain[y][x]; let altitude = f32::max(0.0, cell.altitude); let altitude_modifier = (altitude - (offset_altitude * 1.5) - (offset_altitude_2 * 1.5)) / World::MAX_ALTITUDE; let rainfall_value = mix_values(latitude_modifier_1, altitude_modifier, 0.63); let rainfall = f32::min( World::MAX_RAINFALL, World::calculate_rainfall(rainfall_value), ); cell.rainfall = rainfall; if rainfall > self.max_rainfall { self.max_rainfall = rainfall; } if rainfall < self.min_rainfall { self.min_rainfall = rainfall; } } } info!("Done generating rainfall"); Ok(()) } fn calculate_rainfall(raw_rainfall: f32) -> f32 { f32::clamp( (raw_rainfall * (World::RAINFALL_SPAN + World::RAINFALL_DRYNESS_OFFSET)) + World::MIN_RAINFALL - World::RAINFALL_DRYNESS_OFFSET, 0.0, World::MAX_RAINFALL, ) } fn generate_temperature(&mut self) -> Result<(), CartesianError> { let offset = World::random_offset_vector(&mut self.rng); const RADIUS: f32 = 2.0; for y in 0..self.terrain.len() { let alpha = (y as f32 / self.height as f32) * PI; for x in 0..self.terrain[y].len() { let beta = (x as f32 / self.width as f32) * TAU; let random_noise = self.random_noise_from_polar_coordinates(alpha, beta, RADIUS, offset)?; let cell = &mut self.terrain[y][x]; let latitude_modifer = mix_values(alpha, random_noise * PI, 0.1); let altitude_factor = f32::max( 0.0, (cell.altitude / World::MAX_ALTITUDE) * World::TEMPERATURE_ALTITUDE_FACTOR, ); let temperature = World::calculate_temperature(f32::sin(latitude_modifer) - altitude_factor); cell.temperature = temperature; if temperature > self.max_temperature { self.max_temperature = temperature; } if temperature < self.min_temperature { self.min_temperature = temperature; } } } Ok(()) } fn calculate_temperature(raw_temperature: f32) -> f32 { f32::clamp( (raw_temperature * World::TEMPERATURE_SPAN) + World::MIN_TEMPERATURE, World::MIN_TEMPERATURE, World::MAX_TEMPERATURE, ) } fn generate_biomes(&mut self) { for y in 0..self.terrain.len() { for x in 0..self.terrain[y].len() { let cell = &self.terrain[y][x]; let mut total_presence = 0.0; let mut biome_presences = vec![]; for biome_type in BiomeType::iterator() { let presence = self.biome_presence(cell, &biome_type.into()); if presence <= 0.0 { continue; } biome_presences.push((*biome_type, presence)); total_presence += presence; } self.terrain[y][x].biome_presences = biome_presences .iter() .map(|(biome_type, presence)| (*biome_type, presence / total_presence)) .collect(); } } } fn biome_presence(&self, cell: &TerrainCell, biome: &BiomeStats) -> f32 { let mut presence = 0.0; let altitude_diff = cell.altitude - biome.min_altitude; if altitude_diff < 0.0 { return 0.0; } let altitude_factor = altitude_diff / (biome.max_altitude - biome.min_altitude); if altitude_factor > 1.0 { return 0.0; }; presence += if altitude_factor > 0.5 { 1.0 - altitude_factor } else { altitude_factor }; let rainfall_diff = cell.rainfall - biome.min_rainfall; if rainfall_diff < 0.0 { return 0.0; } let rainfall_factor = rainfall_diff / (biome.max_rainfall - biome.min_rainfall); if rainfall_factor > 1.0 { return 0.0; } presence += if rainfall_factor > 0.5 { 1.0 - rainfall_factor } else { rainfall_factor }; let temperature_diff = cell.temperature - biome.min_temperature; if temperature_diff < 0.0 { return 0.0; } let temperature_factor = temperature_diff / (biome.max_temperature - biome.min_temperature); if temperature_factor > 1.0 { return 0.0; } presence += if temperature_factor > 0.5 { 1.0 - temperature_factor } else { temperature_factor }; presence } #[must_use] pub fn cell_neighbors(&self, x: usize, y: usize) -> HashMap { let mut neighbors = HashMap::new(); let height = self.height as usize; let width = self.width as usize; let north_edge = y >= height - 1; let east_edge = x >= width - 1; let south_edge = y == 0; let west_edge = x == 0; if !north_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::North, (y + 1, x)); } if !north_edge && !east_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::NorthEast, (y + 1, x + 1)); } if !east_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::East, (y, x + 1)); } if !south_edge && !east_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::SouthEast, (y - 1, x + 1)); } if !south_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::South, (y - 1, x)); } if !south_edge && !west_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::SouthWest, (y - 1, x - 1)); } if !west_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::West, (y, x - 1)); }; if !north_edge && !west_edge { _ = neighbors.insert_unique_unchecked(CompassDirection::NorthWest, (y + 1, x - 1)); }; neighbors } #[must_use] pub fn get_slant(&self, x: usize, y: usize) -> f32 { let neighbors = self.cell_neighbors(x, y); let terrain = &self.terrain; let mut west_altitude = f32::MIN; if let Some(neighbor_coords) = neighbors.get(&CompassDirection::North) { west_altitude = f32::max( west_altitude, terrain[neighbor_coords.0][neighbor_coords.1].altitude, ); } if let Some(neighbor_coords) = neighbors.get(&CompassDirection::NorthWest) { west_altitude = f32::max( west_altitude, terrain[neighbor_coords.0][neighbor_coords.1].altitude, ); } if let Some(neighbor_coords) = neighbors.get(&CompassDirection::West) { west_altitude = f32::max( west_altitude, terrain[neighbor_coords.0][neighbor_coords.1].altitude, ); } let mut east_altitude = f32::MIN; if let Some(neighbor_coords) = neighbors.get(&CompassDirection::North) { east_altitude = f32::max( east_altitude, terrain[neighbor_coords.0][neighbor_coords.1].altitude, ); } if let Some(neighbor_coords) = neighbors.get(&CompassDirection::NorthWest) { east_altitude = f32::max( east_altitude, terrain[neighbor_coords.0][neighbor_coords.1].altitude, ); } if let Some(neighbor_coords) = neighbors.get(&CompassDirection::West) { east_altitude = f32::max( east_altitude, terrain[neighbor_coords.0][neighbor_coords.1].altitude, ); } west_altitude - east_altitude } }