diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a55d9a66..d511fdf01e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,12 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a crafting station icon to the crafting menu sidebar for items that could be crafted at a crafting station - Added a setting to disable the hotkey hints - Added a credits screen in the main menu which shows attributions for assets +- Shrubs, a system for spawning smaller tree-like plants into the world. +- Waterfalls +- Sailing boat (currently requires spawning in) ### Changed - Made dungeon tiers 3, 4, and 5 more common - Put date at the begining of the log file instead of the end to allow MIME type recognition - Tweaked CR and exp calculation formula +- Sprite spawn rates ### Removed @@ -27,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The menu map now properly handles dragging the map, zooming, and setting the waypoint when hovering icons - Falling through an airship in flight should no longer be possible (although many issues with airship physics remain) - Avoided black hexagons when bloom is enabled by suppressing NaN/Inf pixels during the first bloom blur pass +- Many know water generation problems ## [0.11.0] - 2021-09-11 diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron index d964d990b4..36184ce50f 100644 --- a/assets/server/manifests/ship_manifest.ron +++ b/assets/server/manifests/ship_manifest.ron @@ -47,4 +47,26 @@ central: ("air_balloon.rudder"), ), ), + SailBoat: ( + bone0: ( + offset: (-6.5, -15.5, 0.0), + phys_offset: (0.0, 0.0, 0.0), + central: ("sail_boat.structure"), + ), + bone1: ( + offset: (0.0, 0.0, 0.0), + phys_offset: (0.0, 0.0, 0.0), + central: ("empty"), + ), + bone2: ( + offset: (0.0, 0.0, 0.0), + phys_offset: (0.0, 0.0, 0.0), + central: ("empty"), + ), + bone3: ( + offset: (-1.5, -6.0, -5.0), + phys_offset: (0.0, 0.0, 0.0), + central: ("empty"), + ), + ), }) diff --git a/assets/server/voxel/sail_boat/structure.vox b/assets/server/voxel/sail_boat/structure.vox new file mode 100644 index 0000000000..023d62b9c6 --- /dev/null +++ b/assets/server/voxel/sail_boat/structure.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17cef56b0257883c2b7ea898a04860706d2c6ec7b18f5c1e9c965164af05ae7c +size 5524 diff --git a/assets/voxygen/shaders/terrain-frag.glsl b/assets/voxygen/shaders/terrain-frag.glsl index 6aff7daaf3..aa5b0e89a2 100644 --- a/assets/voxygen/shaders/terrain-frag.glsl +++ b/assets/voxygen/shaders/terrain-frag.glsl @@ -204,7 +204,7 @@ void main() { const float R_s2s1 = pow((1.0 - 1.3325) / (1.0 + 1.3325), 2); const float R_s1s2 = pow((1.3325 - 1.0) / (1.3325 + 1.0), 2); // float faces_fluid = faces_fluid && f_pos.z <= floor(f_alt); - float fluid_alt = max(f_pos.z + 1, floor(f_alt)); + float fluid_alt = max(f_pos.z + 1, floor(f_alt + 1)); float R_s = /*(f_pos.z < f_alt)*/faces_fluid /*&& f_pos.z <= fluid_alt*/ ? mix(R_s2s1 * R_s1s0, R_s1s0, medium.x) : mix(R_s2s0, R_s1s2 * R_s2s0, medium.x); // vec3 surf_color = /*srgb_to_linear*/(f_col); diff --git a/assets/world/manifests/shrubs/jungle.ron b/assets/world/manifests/shrubs/jungle.ron new file mode 100644 index 0000000000..594c8e4d22 --- /dev/null +++ b/assets/world/manifests/shrubs/jungle.ron @@ -0,0 +1,32 @@ +#![enable(unwrap_newtypes)] + +[ + ( + specifier: "world.shrub.1", + center: (6, 6, 2), + ), + ( + specifier: "world.shrub.jungle.bush-0", + center: (5, 5, 3), + ), + ( + specifier: "world.shrub.jungle.bush-1", + center: (5, 5, 2), + ), + ( + specifier: "world.shrub.jungle.bush-2", + center: (5, 5, 3), + ), + ( + specifier: "world.shrub.jungle.bush-3", + center: (5, 5, 3), + ), + ( + specifier: "world.shrub.jungle.bush-4", + center: (5, 5, 4), + ), + ( + specifier: "world.shrub.jungle.bush-5", + center: (5, 5, 5), + ), +] diff --git a/assets/world/shrub/1.vox b/assets/world/shrub/1.vox new file mode 100644 index 0000000000..8342c6742c --- /dev/null +++ b/assets/world/shrub/1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f08946c4ceebc280dfd30e86eeda045b85f0502620bfbff194e425e9550e64e2 +size 1332 diff --git a/assets/world/shrub/jungle/bush-0.vox b/assets/world/shrub/jungle/bush-0.vox new file mode 100644 index 0000000000..d6fcb4d105 --- /dev/null +++ b/assets/world/shrub/jungle/bush-0.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35c228f2bd6b4e4b11e62016e37e910eeb81f51d1cdf9329a9dbfcf6565e1f5e +size 2444 diff --git a/assets/world/shrub/jungle/bush-1.vox b/assets/world/shrub/jungle/bush-1.vox new file mode 100644 index 0000000000..298ef524d1 --- /dev/null +++ b/assets/world/shrub/jungle/bush-1.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dab85c0bf1c791d0b91038723b9cc79b2f58d2a4832f3d60436e0c1ab3c123f9 +size 2192 diff --git a/assets/world/shrub/jungle/bush-2.vox b/assets/world/shrub/jungle/bush-2.vox new file mode 100644 index 0000000000..32894f9c3a --- /dev/null +++ b/assets/world/shrub/jungle/bush-2.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ab454267e0f243faef48831e81f511df30e3ad0589a34180674a37dd509cfea +size 2012 diff --git a/assets/world/shrub/jungle/bush-3.vox b/assets/world/shrub/jungle/bush-3.vox new file mode 100644 index 0000000000..23d3b19535 --- /dev/null +++ b/assets/world/shrub/jungle/bush-3.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4673b924a97166c519d8e22a1c6d1839cd570c877cd4b7ecd1469adbf88e37eb +size 1968 diff --git a/assets/world/shrub/jungle/bush-4.vox b/assets/world/shrub/jungle/bush-4.vox new file mode 100644 index 0000000000..1fe19a7154 --- /dev/null +++ b/assets/world/shrub/jungle/bush-4.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:280c83933dc0e7d98d17564c33efa95fe2416150171a677975ddc2073d25ce26 +size 2020 diff --git a/assets/world/shrub/jungle/bush-5.vox b/assets/world/shrub/jungle/bush-5.vox new file mode 100644 index 0000000000..6b51e26884 --- /dev/null +++ b/assets/world/shrub/jungle/bush-5.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f05fbe323c57abd5ce550b83f807ed7af3ea297f4eb284cd3b42b48f336f88c +size 3784 diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index d09d78b460..45982c3557 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -854,6 +854,7 @@ impl Body { Body::Ship(ship) => match ship { ship::Body::DefaultAirship => [0.0, 0.0, 10.0], ship::Body::AirBalloon => [0.0, 0.0, 5.0], + ship::Body::SailBoat => [-2.0, -5.0, 4.0], }, _ => [0.0, 0.0, 0.0], } diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs index 09bae038e0..1c3a28570b 100644 --- a/common/src/comp/body/ship.rs +++ b/common/src/comp/body/ship.rs @@ -1,13 +1,13 @@ use crate::{ comp::{Density, Mass}, - consts::AIR_DENSITY, + consts::{AIR_DENSITY, WATER_DENSITY}, make_case_elim, }; use rand::prelude::SliceRandom; use serde::{Deserialize, Serialize}; use vek::Vec3; -pub const ALL_BODIES: [Body; 2] = [Body::DefaultAirship, Body::AirBalloon]; +pub const ALL_BODIES: [Body; 3] = [Body::DefaultAirship, Body::AirBalloon, Body::SailBoat]; make_case_elim!( body, @@ -16,6 +16,7 @@ make_case_elim!( pub enum Body { DefaultAirship = 0, AirBalloon = 1, + SailBoat = 2, } ); @@ -35,6 +36,7 @@ impl Body { match self { Body::DefaultAirship => "airship_human.structure", Body::AirBalloon => "air_balloon.structure", + Body::SailBoat => "sail_boat.structure", } } @@ -42,15 +44,21 @@ impl Body { match self { Body::DefaultAirship => Vec3::new(25.0, 50.0, 40.0), Body::AirBalloon => Vec3::new(25.0, 50.0, 40.0), + Body::SailBoat => Vec3::new(13.0, 31.0, 3.0), } } fn balloon_vol(&self) -> f32 { - let spheroid_vol = |equat_d: f32, polar_d: f32| -> f32 { - (std::f32::consts::PI / 6.0) * equat_d.powi(2) * polar_d - }; - let dim = self.dimensions(); - spheroid_vol(dim.z, dim.y) + match self { + Body::DefaultAirship | Body::AirBalloon => { + let spheroid_vol = |equat_d: f32, polar_d: f32| -> f32 { + (std::f32::consts::PI / 6.0) * equat_d.powi(2) * polar_d + }; + let dim = self.dimensions(); + spheroid_vol(dim.z, dim.y) + }, + _ => 0.0, + } } fn hull_vol(&self) -> f32 { @@ -66,14 +74,19 @@ impl Body { Density(ratio * oak_density + (1.0 - ratio) * AIR_DENSITY) } - pub fn density(&self) -> Density { Density(AIR_DENSITY) } + pub fn density(&self) -> Density { + match self { + Body::DefaultAirship | Body::AirBalloon => Density(AIR_DENSITY), + _ => Density(AIR_DENSITY * 0.8 + WATER_DENSITY * 0.2), // Most boats should be buoyant + } + } pub fn mass(&self) -> Mass { Mass((self.hull_vol() + self.balloon_vol()) * self.density().0) } - pub fn can_fly(&self) -> bool { - match self { - Body::DefaultAirship | Body::AirBalloon => true, - } + pub fn can_fly(&self) -> bool { matches!(self, Body::DefaultAirship | Body::AirBalloon) } + + pub fn has_water_thrust(&self) -> bool { + !self.can_fly() // TODO: Differentiate this more carefully } } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index 9d1d4ffb69..3b554b4f07 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -173,6 +173,7 @@ impl Body { quadruped_low::Species::Lavadrake => 1.7, _ => 2.0, }, + Body::Ship(ship) if ship.has_water_thrust() => 0.1, Body::Ship(_) => 0.035, } } @@ -180,7 +181,7 @@ impl Body { /// Returns thrust force if the body type can swim, otherwise None pub fn swim_thrust(&self) -> Option { match self { - Body::Object(_) | Body::Ship(_) => None, + Body::Object(_) => None, Body::BipedLarge(_) | Body::Golem(_) => Some(200.0 * self.mass().0), Body::BipedSmall(_) => Some(100.0 * self.mass().0), Body::BirdMedium(_) => Some(50.0 * self.mass().0), @@ -200,6 +201,8 @@ impl Body { Body::QuadrupedLow(_) => Some(300.0 * self.mass().0), Body::QuadrupedMedium(_) => Some(300.0 * self.mass().0), Body::QuadrupedSmall(_) => Some(300.0 * self.mass().0), + Body::Ship(ship) if ship.has_water_thrust() => Some(1500.0 * self.mass().0), + Body::Ship(_) => None, } } diff --git a/common/src/terrain/map.rs b/common/src/terrain/map.rs index 7f6550e02f..3f23b0fd53 100644 --- a/common/src/terrain/map.rs +++ b/common/src/terrain/map.rs @@ -551,8 +551,11 @@ impl<'a> MapConfig<'a> { downhill_wpos, ); let (_t, _pt, dist) = if let Some((t, pt, dist)) = - quadratic_nearest_point(&coeffs, wposf) - { + quadratic_nearest_point( + &coeffs, + wposf, + Vec2::new(neighbor_wpos, downhill_wpos), + ) { (t, pt, dist) } else { let ndist = wposf.distance_squared(neighbor_wpos); diff --git a/common/src/terrain/mod.rs b/common/src/terrain/mod.rs index 0bb91f5c9b..d5d8d87042 100644 --- a/common/src/terrain/mod.rs +++ b/common/src/terrain/mod.rs @@ -293,7 +293,53 @@ pub fn river_spline_coeffs( pub fn quadratic_nearest_point( spline: &Vec3>, point: Vec2, + _line: Vec2>, // Used for alternative distance functions below ) -> Option<(f64, Vec2, f64)> { + //let eval_at = |t: f64| spline.x * t * t + spline.y * t + spline.z; + + // Linear + + // let line = LineSegment2 { + // start: line.x, + // end: line.y, + // }; + // let len_sq = line.start.distance_squared(line.end); + // let t = ((point - line.start).dot(line.end - line.start) / + // len_sq).clamped(0.0, 1.0); let pos = line.start + (line.end - line.start) + // * t; return Some((t, pos, pos.distance_squared(point))); + + // Quadratic + + // let curve = QuadraticBezier2 { + // start: line.x, + // ctrl: eval_at(0.5), + // end: line.y, + // }; + // let (t, pos) = curve.binary_search_point_by_steps(point, 16, 0.001); + // let t = t.clamped(0.0, 1.0); + // let pos = curve.evaluate(t); + // return Some((t, pos, pos.distance_squared(point))); + + // Cubic + + // let ctrl_at = |t: f64, end: f64| { + // let a = eval_at(end); + // let b = eval_at(Lerp::lerp(end, t, 0.1)); + // let dir = (b - a).normalized(); + // let exact = eval_at(t); + // a + dir * exact.distance(a) + // }; + // let curve = CubicBezier2 { + // start: line.x, + // ctrl0: ctrl_at(0.33, 0.0), + // ctrl1: ctrl_at(0.66, 1.0), + // end: line.y, + // }; + // let (t, pos) = curve.binary_search_point_by_steps(point, 12, 0.01); + // let t = t.clamped(0.0, 1.0); + // let pos = curve.evaluate(t); + // return Some((t, pos, pos.distance_squared(point))); + let a = spline.z.x; let b = spline.y.x; let c = spline.x.x; @@ -328,18 +374,14 @@ pub fn quadratic_nearest_point( let min_root = roots .iter() .copied() - .filter_map(|root| { + .map(|root| { let river_point = spline.x * root * root + spline.y * root + spline.z; - let river_zero = spline.z; - let river_one = spline.x + spline.y + spline.z; if root > 0.0 && root < 1.0 { - Some((root, river_point)) - } else if river_point.distance_squared(river_zero) < 0.5 { - Some((root, /*river_point*/ river_zero)) - } else if river_point.distance_squared(river_one) < 0.5 { - Some((root, /*river_point*/ river_one)) + (root, river_point) } else { - None + let root = root.clamped(0.0, 1.0); + let river_point = spline.x * root * root + spline.y * root + spline.z; + (root, river_point) } }) .map(|(root, river_point)| { diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs index afecb5e600..a1afe338fa 100644 --- a/voxygen/anim/src/ship/mod.rs +++ b/voxygen/anim/src/ship/mod.rs @@ -31,7 +31,9 @@ impl Skeleton for ShipSkeleton { buf: &mut [FigureBoneData; super::MAX_BONE_COUNT], body: Self::Body, ) -> Offsets { - let bone0_mat = base_mat * Mat4::scaling_3d(1.0 / 11.0) * Mat4::::from(self.bone0); + let scale_mat = Mat4::scaling_3d(1.0 / 11.0); + + let bone0_mat = base_mat * scale_mat * Mat4::::from(self.bone0); *(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [ make_bone(bone0_mat), @@ -43,10 +45,12 @@ impl Skeleton for ShipSkeleton { lantern: None, // TODO: see quadruped_medium for how to animate this mount_bone: Transform { - position: common::comp::Body::Ship(body) - .mountee_offset() - .into_tuple() - .into(), + position: (base_mat * scale_mat).mul_point( + common::comp::Body::Ship(body) + .mountee_offset() + .into_tuple() + .into(), + ), ..Default::default() }, } @@ -89,18 +93,22 @@ impl<'a> From<&'a Body> for SkeletonAttr { bone0: match body { DefaultAirship => (0.0, 0.0, 0.0), AirBalloon => (0.0, 0.0, 0.0), + SailBoat => (0.0, 0.0, 0.0), }, bone1: match body { DefaultAirship => (-13.0, -25.0, 10.0), AirBalloon => (0.0, 0.0, 0.0), + SailBoat => (0.0, 0.0, 0.0), }, bone2: match body { DefaultAirship => (13.0, -25.0, 10.0), AirBalloon => (0.0, 0.0, 0.0), + SailBoat => (0.0, 0.0, 0.0), }, bone3: match body { DefaultAirship => (0.0, -27.5, 8.5), AirBalloon => (0.0, -9.0, 8.0), + SailBoat => (0.0, 0.0, 0.0), }, } } diff --git a/world/src/all.rs b/world/src/all.rs index 59251a5e62..b5acc3bfb6 100644 --- a/world/src/all.rs +++ b/world/src/all.rs @@ -35,7 +35,7 @@ impl ForestKind { ForestKind::Cedar => 0.275..1.45, ForestKind::Pine => 0.2..1.4, ForestKind::Birch => 0.0..0.6, - ForestKind::Mangrove => 0.65..1.3, + ForestKind::Mangrove => 0.5..1.3, ForestKind::Swamp => 0.5..1.1, _ => 0.0..0.0, } @@ -51,7 +51,7 @@ impl ForestKind { ForestKind::Cedar => -0.65..0.15, ForestKind::Pine => -1.8..-0.2, ForestKind::Birch => -0.7..0.25, - ForestKind::Mangrove => 0.4..1.6, + ForestKind::Mangrove => 0.35..1.6, ForestKind::Swamp => -0.6..0.8, _ => 0.0..0.0, } diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index d1ce83457f..ad9219d54b 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -147,23 +147,27 @@ impl<'a> BlockGen<'a> { } else { Some(Block::new(BlockKind::Earth, col)) } - } else if (wposf.z as f32) < height { + } else if wposf.z as i32 <= height as i32 { let grass_factor = (wposf.z as f32 - (height - grass_depth)) .div(grass_depth) .sqrt(); - let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor); // Surface - Some(Block::new( - if snow_cover { + Some(if water_level > height.ceil() { + Block::new( + BlockKind::Sand, + sub_surface_color.map(|e| (e * 255.0) as u8), + ) + } else { + let col = Lerp::lerp(sub_surface_color, surface_color, grass_factor); + if grass_factor < 0.7 { + Block::new(BlockKind::Earth, col.map(|e| (e * 255.0) as u8)) + } else if snow_cover { //if temp < CONFIG.snow_temp + 0.031 { - BlockKind::Snow - } else if grass_factor > 0.7 { - BlockKind::Grass + Block::new(BlockKind::Snow, col.map(|e| (e * 255.0) as u8)) } else { - BlockKind::Earth - }, - col.map(|e| (e * 255.0) as u8), - )) + Block::new(BlockKind::Grass, col.map(|e| (e * 255.0) as u8)) + } + }) } else { None } diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index d57e5892d2..4fa873f159 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -1,7 +1,7 @@ use crate::{ all::ForestKind, sim::{local_cells, Cave, Path, RiverKind, SimChunk, WorldSim}, - util::Sampler, + util::{RandomField, Sampler}, IndexRef, CONFIG, }; use common::{ @@ -14,7 +14,6 @@ use common::{ use noise::NoiseFn; use serde::Deserialize; use std::{ - cmp::Reverse, f32, ops::{Add, Div, Mul, Sub}, }; @@ -51,6 +50,15 @@ pub struct Colors { pub tropical_high: (f32, f32, f32), } +/// Generalised power function, pushes values in the range 0-1 to extremes. +fn power(x: f64, t: f64) -> f64 { + if x < 0.5 { + (2.0 * x).powf(t) / 2.0 + } else { + 1.0 - (-2.0 * x + 2.0).powf(t) / 2.0 + } +} + impl<'a> ColumnGen<'a> { pub fn new(sim: &'a WorldSim) -> Self { Self { sim } } } @@ -96,172 +104,647 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { Some((neighbor_pos, neighbor_chunk, &neighbor_chunk.river)) }); - let lake_width = (TerrainChunkSize::RECT_SIZE.x as f64 * (2.0f64.sqrt())) + 12.0; - let neighbor_river_data = neighbor_river_data.map(|(posj, chunkj, river)| { - let kind = match river.river_kind { - Some(kind) => kind, - None => { - return (posj, chunkj, river, None); - }, - }; - let downhill_pos = if let Some(pos) = chunkj.downhill { - pos - } else { - match kind { - RiverKind::River { .. } => { - error!(?river, ?posj, "What?"); - panic!("How can a river have no downhill?"); - }, - RiverKind::Lake { .. } => { + let gradient = sim.get_gradient_approx(chunk_pos); + + let lake_width = (TerrainChunkSize::RECT_SIZE.x as f64 * 2.0f64.sqrt()) + 6.0; + let neighbor_river_data = neighbor_river_data + .map(|(posj, chunkj, river)| { + let kind = match river.river_kind { + Some(kind) => kind, + None => { return (posj, chunkj, river, None); }, - RiverKind::Ocean => posj, - } - }; - let downhill_wpos = downhill_pos.map(|e| e as f64); - let downhill_pos = - downhill_pos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); - let neighbor_pos = posj.map(|e| e as f64) * neighbor_coef; - let direction = neighbor_pos - downhill_wpos; - let river_width_min = if let RiverKind::River { cross_section } = kind { - cross_section.x as f64 - } else { - lake_width - }; - let downhill_chunk = sim.get(downhill_pos).expect("How can this not work?"); - let coeffs = - river_spline_coeffs(neighbor_pos, chunkj.river.spline_derivative, downhill_wpos); - let (direction, coeffs, downhill_chunk, river_t, river_pos, river_dist) = match kind { - RiverKind::River { .. } => { - if let Some((t, pt, dist)) = quadratic_nearest_point(&coeffs, wposf) { - (direction, coeffs, downhill_chunk, t, pt, dist.sqrt()) - } else { - let ndist = wposf.distance_squared(neighbor_pos); - let ddist = wposf.distance_squared(downhill_wpos); - let (closest_pos, closest_dist, closest_t) = if ndist <= ddist { - (neighbor_pos, ndist, 0.0) - } else { - (downhill_wpos, ddist, 1.0) - }; - ( - direction, - coeffs, - downhill_chunk, - closest_t, - closest_pos, - closest_dist.sqrt(), - ) + }; + let downhill_pos = if let Some(pos) = chunkj.downhill { + pos + } else { + match kind { + RiverKind::River { .. } => { + error!(?river, ?posj, "What?"); + panic!("How can a river have no downhill?"); + }, + RiverKind::Lake { .. } => { + return (posj, chunkj, river, None); + }, + RiverKind::Ocean => posj, } - }, - RiverKind::Lake { neighbor_pass_pos } => { - let pass_dist = neighbor_pass_pos - .map2( - neighbor_pos - .map2(TerrainChunkSize::RECT_SIZE, |f, g| (f as i32, g as i32)), - |e, (f, g)| ((e - f) / g).abs(), - ) - .reduce_partial_max(); - let spline_derivative = river.spline_derivative; - let neighbor_pass_pos = if pass_dist <= 1 { - neighbor_pass_pos - } else { - downhill_wpos.map(|e| e as i32) - }; - let pass_dist = neighbor_pass_pos - .map2( - neighbor_pos - .map2(TerrainChunkSize::RECT_SIZE, |f, g| (f as i32, g as i32)), - |e, (f, g)| ((e - f) / g).abs(), - ) - .reduce_partial_max(); - if pass_dist > 1 { - return (posj, chunkj, river, None); - } - let neighbor_pass_wpos = neighbor_pass_pos.map(|e| e as f64); - let neighbor_pass_pos = neighbor_pass_pos - .map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); - let coeffs = - river_spline_coeffs(neighbor_pos, spline_derivative, neighbor_pass_wpos); - let direction = neighbor_pos - neighbor_pass_wpos; - if let Some((t, pt, dist)) = quadratic_nearest_point(&coeffs, wposf) { - ( - direction, - coeffs, - sim.get(neighbor_pass_pos).expect("Must already work"), - t, - pt, - dist.sqrt(), - ) - } else { - let ndist = wposf.distance_squared(neighbor_pos); - /* let ddist = wposf.distance_squared(neighbor_pass_wpos); */ - let (closest_pos, closest_dist, closest_t) = /*if ndist <= ddist */ { - (neighbor_pos, ndist, 0.0) - } /* else { - (neighbor_pass_wpos, ddist, 1.0) - } */; - ( - direction, - coeffs, - sim.get(neighbor_pass_pos).expect("Must already work"), - closest_t, - closest_pos, - closest_dist.sqrt(), - ) - } - }, - RiverKind::Ocean => { - let ndist = wposf.distance_squared(neighbor_pos); - let (closest_pos, closest_dist, closest_t) = (neighbor_pos, ndist, 0.0); - ( - direction, - coeffs, - sim.get(closest_pos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { - e as i32 / sz as i32 - })) - .expect("Must already work"), - closest_t, - closest_pos, - closest_dist.sqrt(), - ) - }, - }; - let river_width_max = - if let Some(RiverKind::River { cross_section }) = downhill_chunk.river.river_kind { + }; + let downhill_wpos = downhill_pos.map(|e| e as f64); + let downhill_pos = downhill_pos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { + e.div_euclid(sz as i32) + }); + let neighbor_wpos = posj.map(|e| e as f64) * neighbor_coef + neighbor_coef * 0.5; + let direction = neighbor_wpos - downhill_wpos; + let river_width_min = if let RiverKind::River { cross_section } = kind { cross_section.x as f64 } else { lake_width }; - let river_width_noise = (sim.gen_ctx.small_nz.get((river_pos.div(16.0)).into_array())) - .max(-1.0) - .min(1.0) - .mul(0.5) - .sub(0.5) as f64; - let river_width = Lerp::lerp( - river_width_min, - river_width_max, - river_t.max(0.0).min(1.0).sqrt(), - ); + let downhill_chunk = sim.get(downhill_pos).expect("How can this not work?"); + let coeffs = river_spline_coeffs( + neighbor_wpos, + chunkj.river.spline_derivative, + downhill_wpos, + ); + let (direction, coeffs, downhill_chunk, river_t, river_pos, river_dist) = match kind + { + RiverKind::River { .. } => { + if let Some((t, pt, dist)) = quadratic_nearest_point( + &coeffs, + wposf, + Vec2::new(neighbor_wpos, downhill_wpos), + ) { + let (t, pt, dist) = if dist > wposf.distance_squared(neighbor_wpos) { + (0.0, neighbor_wpos, wposf.distance_squared(neighbor_wpos)) + } else if dist > wposf.distance_squared(downhill_wpos) { + (1.0, downhill_wpos, wposf.distance_squared(downhill_wpos)) + } else { + (t, pt, dist) + }; + (direction, coeffs, downhill_chunk, t, pt, dist.sqrt()) + } else { + let ndist = wposf.distance_squared(neighbor_wpos); + let ddist = wposf.distance_squared(downhill_wpos); + let (closest_pos, closest_dist, closest_t) = if ndist <= ddist { + (neighbor_wpos, ndist, 0.0) + } else { + (downhill_wpos, ddist, 1.0) + }; + ( + direction, + coeffs, + downhill_chunk, + closest_t, + closest_pos, + closest_dist.sqrt(), + ) + } + }, + RiverKind::Lake { neighbor_pass_pos } => { + let pass_dist = neighbor_pass_pos + .map2( + neighbor_wpos + .map2(TerrainChunkSize::RECT_SIZE, |f, g| (f as i32, g as i32)), + |e, (f, g)| ((e - f) / g).abs(), + ) + .reduce_partial_max(); + let spline_derivative = river.spline_derivative; + let neighbor_pass_pos = if pass_dist <= 1 { + neighbor_pass_pos + } else { + downhill_wpos.map(|e| e as i32) + }; + let pass_dist = neighbor_pass_pos + .map2( + neighbor_wpos + .map2(TerrainChunkSize::RECT_SIZE, |f, g| (f as i32, g as i32)), + |e, (f, g)| ((e - f) / g).abs(), + ) + .reduce_partial_max(); + if pass_dist > 1 { + return (posj, chunkj, river, None); + } + let neighbor_pass_wpos = + neighbor_pass_pos.map(|e| e as f64) + neighbor_coef * 0.5; + let neighbor_pass_pos = neighbor_pass_pos + .map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); + let coeffs = river_spline_coeffs( + neighbor_wpos, + spline_derivative, + neighbor_pass_wpos, + ); + let direction = neighbor_wpos - neighbor_pass_wpos; - let river_width = river_width * (1.0 + river_width_noise * 0.3); - // To find the distance, we just evaluate the quadratic equation at river_t and - // see if it's within width (but we should be able to use it for a - // lot more, and this probably isn't the very best approach anyway - // since it will bleed out). let river_pos = coeffs.x * river_t * - // river_t + coeffs.y * river_t + coeffs.z; - let res = Vec2::new(0.0, (river_dist - (river_width * 0.5).max(1.0)).max(0.0)); + // Lakes get a special distance function to avoid cookie-cutter edges + if matches!( + downhill_chunk.river.river_kind, + Some(RiverKind::Lake { .. } | RiverKind::Ocean) + ) { + let water_chunk = posj.map(|e| e as f64); + let lake_width_noise = sim + .gen_ctx + .small_nz + .get((wposf.map(|e| e as f64).div(32.0)).into_array()); + let water_aabr = Aabr { + min: water_chunk * neighbor_coef + 4.0 - lake_width_noise * 8.0, + max: (water_chunk + 1.0) * neighbor_coef - 4.0 + + lake_width_noise * 8.0, + }; + let pos = water_aabr.projected_point(wposf); + ( + direction, + coeffs, + sim.get(neighbor_pass_pos).expect("Must already work"), + 0.5, + pos, + pos.distance(wposf), + ) + } else if let Some((t, pt, dist)) = quadratic_nearest_point( + &coeffs, + wposf, + Vec2::new(neighbor_wpos, neighbor_pass_wpos), + ) { + ( + direction, + coeffs, + sim.get(neighbor_pass_pos).expect("Must already work"), + t, + pt, + dist.sqrt(), + ) + } else { + let ndist = wposf.distance_squared(neighbor_wpos); + /* let ddist = wposf.distance_squared(neighbor_pass_wpos); */ + let (closest_pos, closest_dist, closest_t) = /*if ndist <= ddist */ { + (neighbor_wpos, ndist, 0.0) + } /* else { + (neighbor_pass_wpos, ddist, 1.0) + } */; + ( + direction, + coeffs, + sim.get(neighbor_pass_pos).expect("Must already work"), + closest_t, + closest_pos, + closest_dist.sqrt(), + ) + } + }, + RiverKind::Ocean => { + let water_chunk = posj.map(|e| e as f64); + let lake_width_noise = sim + .gen_ctx + .small_nz + .get((wposf.map(|e| e as f64).div(32.0)).into_array()); + let water_aabr = Aabr { + min: water_chunk * neighbor_coef + 4.0 - lake_width_noise * 8.0, + max: (water_chunk + 1.0) * neighbor_coef - 4.0 + lake_width_noise * 8.0, + }; + let pos = water_aabr.projected_point(wposf); + ( + direction, + coeffs, + sim.get(posj).expect("Must already work"), + 0.5, + pos, + pos.distance(wposf), + ) + }, + }; + let river_width_max = if let Some(RiverKind::River { cross_section }) = + downhill_chunk.river.river_kind + { + // Harmless hack that prevents a river growing wildly outside its bounds to + // create water walls + (cross_section.x as f64).min(river_width_min * 1.75) + } else if let Some(RiverKind::River { cross_section }) = chunkj.river.river_kind { + Lerp::lerp(cross_section.x as f64, lake_width, 0.5) + } else { + // 0.5 prevents rivers pooling into lakes having extremely wide bounds, creating + // water walls + lake_width * 0.5 + }; + let river_width_noise = + (sim.gen_ctx.small_nz.get((river_pos.div(16.0)).into_array())) + .max(-1.0) + .min(1.0) + .mul(0.5) + .sub(0.5) as f64; + let river_width = Lerp::lerp( + river_width_min, + river_width_max, + river_t.clamped(0.0, 1.0).powf(3.0), + ); + + let river_width = river_width.max(2.0) * (1.0 + river_width_noise * 0.3); + // To find the distance, we just evaluate the quadratic equation at river_t and + // see if it's within width (but we should be able to use it for a + // lot more, and this probably isn't the very best approach anyway + // since it will bleed out). let river_pos = coeffs.x * river_t * + // river_t + coeffs.y * river_t + coeffs.z; + // let river_width = 32.0f64; + let res = Vec2::new(0.0, (river_dist - (river_width * 0.5).max(1.0)).max(0.0)); + ( + posj, + chunkj, + river, + Some(( + direction, + res, + river_width, + (river_t, (river_pos, coeffs), downhill_chunk), + )), + ) + }) + .collect::>(); + + debug_assert!(sim_chunk.water_alt >= CONFIG.sea_level); + + /// A type that makes managing surface altitude weighting much simpler. + #[derive(Default)] + struct WeightedSum { + sum: T, + weight: T, + min: Option, + max: Option, + } + impl WeightedSum { + /// Add a weight to the sum. + fn with(self, value: f32, weight: f32) -> Self { + Self { + sum: self.sum + value * weight, + weight: self.weight + weight, + ..self + } + } + + /// Add an upper bound to the result. + fn with_min(self, min: f32) -> Self { + Self { + min: Some(self.min.unwrap_or(min).min(min)), + ..self + } + } + + /// Add a lower bound to the result. + fn with_max(self, max: f32) -> Self { + Self { + max: Some(self.max.unwrap_or(max).max(max)), + ..self + } + } + + /// Evaluate the weighted sum, if weightings were applied. + fn eval(&self) -> Option { + if self.weight > 0.0 { + let res = self.sum / self.weight; + let res = self.min.map_or(res, |m| m.min(res)); + let res = self.max.map_or(res, |m| m.max(res)); + Some(res) + } else { + None + } + } + + /// Evaluate the weighted sum, or use a default value if no + /// weightings were applied. + fn eval_or(&self, default: f32) -> f32 { + let res = if self.weight > 0.0 { + self.sum / self.weight + } else { + default + }; + let res = self.min.map_or(res, |m| m.min(res)); + self.max.map_or(res, |m| m.max(res)) + } + } + + /// Determine whether a river should become a waterfall + fn is_waterfall( + chunk_pos: Vec2, + river_chunk: &SimChunk, + downhill_chunk: &SimChunk, + ) -> bool { + // Waterfalls are rare, so use some hacky RNG seeded with the position to + // reflect that. Additionally, the river must experience a rapid + // change in elevation. Pooling into a lake produces a rapid. + // TODO: Find a better way to produce rapids along the course of a river? + (RandomField::new(3119).chance(chunk_pos.with_z(0), 0.1) + || matches!( + downhill_chunk.river.river_kind, + Some(RiverKind::Lake { .. }) + )) + && (river_chunk.water_alt > downhill_chunk.water_alt + 8.0) + } + + /// Determine the altitude of a river based on the altitude of the + /// spline ends and a tweening factor. + fn river_water_alt(a: f32, b: f32, t: f32, is_waterfall: bool) -> f32 { + let t = if is_waterfall { + // Waterfalls bias the water altitude toward extremes + power(t as f64, 3.0 + (a - b).clamped(0.0, 16.0) as f64) as f32 + } else { + t + }; + Lerp::lerp(a, b, t) + } + + // Use this to temporarily alter the sea level + let base_sea_level = CONFIG.sea_level - 1.0 + 0.01; + + // What's going on here? + // + // We're iterating over nearby bodies of water and calculating a weighted sum + // for the river water level, the lake water level, and the 'unbounded + // water level' (the maximum water body altitude, which we use later to + // prevent water walls). In doing so, we also apply various clamping strategies + // to catch lots of nasty edge cases, as well as calculating the + // distance to the nearest body of water. + // + // The clamping strategies employed prevent very specific, annoying artifacts + // such as 'water walls' (vertical columns of water that are physically + // implausible) and 'backflows' (regions where a body of water appears to + // flow upstream due to irregular humps along its course). + // + // It is incredibly difficult to explain exactly what every part of this code is + // doing without visual examples. Needless to say, any changes to this + // code *at all* should be very ruggedly tested to ensure that + // they do not result in artifacts, even in edge cases. The exact configuration + // of this code is the product of hundreds of hours of testing and + // refinement and I ask that you do not take that effort lightly. + let (river_water_level, in_river, lake_water_level, lake_dist, water_dist, unbounded_water_level) = neighbor_river_data.iter().copied().fold( ( - posj, - chunkj, - river, - Some(( - direction, - res, - river_width, - (river_t, (river_pos, coeffs), downhill_chunk), - )), - ) - }); + WeightedSum::default().with_max(base_sea_level), + false, + WeightedSum::default().with_max(base_sea_level), + 10000.0f32, + None, + WeightedSum::default().with_max(base_sea_level), + ), + |(mut river_water_level, mut in_river, mut lake_water_level, mut lake_dist, water_dist, mut unbounded_water_level), (river_chunk_idx, river_chunk, river, dist_info)| match (river.river_kind, dist_info) { + ( + Some(kind), + Some((_, _, river_width, (river_t, (river_pos, _), downhill_chunk))), + ) => { + // Distance from river center + let river_dist = river_pos.distance(wposf); + // Distance from edge of river + let river_edge_dist = (river_dist - river_width * 0.5).max(0.0) as f32; + // 0.0 = not near river, 1.0 = in middle of river + let near_center = ((river_dist / (river_width * 0.5)) as f32) + .min(1.0) + .mul(f32::consts::PI) + .cos() + .add(1.0) + .mul(0.5); + + match kind { + RiverKind::River { .. } => { + // Alt of river water *is* the alt of land (ignoring gorge, which gets applied later) + let river_water_alt = river_water_alt( + river_chunk.alt.max(river_chunk.water_alt), + downhill_chunk.alt.max(downhill_chunk.water_alt), + river_t as f32, + is_waterfall(river_chunk_idx, river_chunk, downhill_chunk), + ); + + river_water_level = river_water_level + .with(river_water_alt, near_center); + + if river_edge_dist <= 0.0 { + in_river = true; + } + }, + // Slightly wider threshold is chosen in case the lake bounds are a bit wrong + RiverKind::Lake { .. } | RiverKind::Ocean => { + let lake_water_alt = if matches!(kind, RiverKind::Ocean) { + base_sea_level + } else { + river_water_alt( + river_chunk.alt.max(river_chunk.water_alt), + downhill_chunk.alt.max(downhill_chunk.water_alt), + river_t as f32, + is_waterfall(river_chunk_idx, river_chunk, downhill_chunk), + ) + }; + + if river_edge_dist > 0.0 && river_width > lake_width * 0.99 { + let unbounded_water_alt = lake_water_alt - ((river_edge_dist - 8.0).max(0.0) / 5.0).powf(2.0); + unbounded_water_level = unbounded_water_level + .with(unbounded_water_alt, 1.0 / (1.0 + river_edge_dist * 5.0)) + .with_max(unbounded_water_alt); + } + + river_water_level = river_water_level + .with(lake_water_alt, near_center); + + lake_dist = lake_dist.min(river_edge_dist); + + // Lake border prevents a lake failing to propagate its altitude to nearby rivers + if river_edge_dist <= 0.0 { + lake_water_level = lake_water_level + // Make sure the closest lake is prioritised + .with(lake_water_alt, near_center + 0.1 / (1.0 + river_edge_dist)); + } + }, + }; + + let river_edge_dist_unclamped = (river_dist - river_width * 0.5) as f32; + let water_dist = Some(water_dist.unwrap_or(river_edge_dist_unclamped).min(river_edge_dist_unclamped)); + + (river_water_level, in_river, lake_water_level, lake_dist, water_dist, unbounded_water_level) + }, + (_, _) => (river_water_level, in_river, lake_water_level, lake_dist, water_dist, unbounded_water_level), + }, + ); + let unbounded_water_level = unbounded_water_level.eval_or(base_sea_level); + // Calculate a final, canonical altitude for the water in this column by + // combining and clamping the attributes we found while iterating over + // nearby bodies of water. + let water_level = match ( + river_water_level.eval(), + lake_water_level + .eval() + .filter(|_| lake_dist <= 0.0 || in_river), + ) { + (Some(r), Some(l)) => r.max(l), + (r, l) => r.or(l).unwrap_or(base_sea_level).max(unbounded_water_level), + } + .max(base_sea_level); + + let riverless_alt = alt; + + // What's going on here? + // + // Now that we've figured out the altitude of the water in this column, we can + // determine the altitude of the river banks. This initially appears + // somewhat backward (surely the river basin determines the water level?) + // but it is necessary to prevent backflows. Here, the surface of the water is + // king because we require global information to determine it without + // backflows. The river banks simply reflect the will of the water. We care + // much less about a river bank that's slightly rugged and irregular than we do + // about the surface of the water itself being rugged and irregular (and + // hence physically implausible). From that perspective, it makes sense + // that we determine river banks after the water level because it is the one + // that we are most at liberty to screw up. + // + // Similar to the iteration above, we perform a fold over nearby bodies of water + // and use the distance to the water to come up wight a weighted sum for + // the altitude. The way we determine this altitude differs somewhat + // between rivers, lakes, and the ocean and also whether we are *inside* said + // bodies of water or simply near their edge. + // + // As with the previous iteration, a lot of this code is extremely delicate and + // has been carefully designed to handle innumeral edge cases. Please + // test any changes to this code extremely well to avoid regressions: some + // edge cases are very rare indeed! + let alt = neighbor_river_data.into_iter().fold( + WeightedSum::default().with(riverless_alt, 1.0), + |alt, (river_chunk_idx, river_chunk, river, dist_info)| match ( + river.river_kind, + dist_info, + ) { + ( + Some(kind), + Some((_, _, river_width, (river_t, (river_pos, _), downhill_chunk))), + ) => { + // Distance from river center + let river_dist = river_pos.distance(wposf); + // Distance from edge of river + let river_edge_dist = (river_dist - river_width * 0.5).max(0.0) as f32; + + let water_alt = match kind { + RiverKind::River { cross_section } => { + // Alt of river water *is* the alt of land + let river_water_alt = river_water_alt( + river_chunk.alt.max(river_chunk.water_alt), + downhill_chunk.alt.max(downhill_chunk.water_alt), + river_t as f32, + is_waterfall(river_chunk_idx, river_chunk, downhill_chunk), + ); + Some((river_water_alt, cross_section.y as f32, None)) + }, + RiverKind::Lake { .. } | RiverKind::Ocean => { + let lake_water_alt = if matches!(kind, RiverKind::Ocean) { + base_sea_level + } else { + river_water_alt( + river_chunk.alt.max(river_chunk.water_alt), + downhill_chunk.alt.max(downhill_chunk.water_alt), + river_t as f32, + is_waterfall(river_chunk_idx, river_chunk, downhill_chunk), + ) + }; + + let depth = water_level + - Lerp::lerp( + riverless_alt.min(water_level), + water_level - 4.0, + 0.5, + ); + + let min_alt = Lerp::lerp( + riverless_alt, + lake_water_alt, + ((river_dist / (river_width * 0.5) - 0.5) * 2.0).clamped(0.0, 1.0) + as f32, + ); + + Some(( + lake_water_alt, + // TODO: The depth given to us by the erosion code is technically + // correct, but it also + // looks terrible. Come up with a good solution to this. + /* river_width as f32 * 0.15 */ + depth, + Some(min_alt), + )) + }, + }; + + const BANK_STRENGTH: f32 = 100.0; + if let Some((water_alt, water_depth, min_alt)) = water_alt { + if river_edge_dist <= 0.0 { + const MIN_DEPTH: f32 = 1.0; + let near_center = ((river_dist / (river_width * 0.5)) as f32) + .min(1.0) + .mul(f32::consts::PI) + .cos() + .add(1.0) + .mul(0.5); + // Waterfalls 'boost' the depth of the river to prevent artifacts. This + // is also necessary when rivers become very + // steep without explicitly being waterfalls. + // TODO: Come up with a more principled way of doing this without + // guessing magic numbers + let waterfall_boost = + if is_waterfall(river_chunk_idx, river_chunk, downhill_chunk) { + (river_chunk.alt - downhill_chunk.alt).max(0.0).powf(2.0) + * (1.0 - (river_t as f32 - 0.5).abs() * 2.0).powf(3.5) + / 20.0 + } else { + // Handle very steep rivers gracefully + (river_chunk.alt - downhill_chunk.alt).max(0.0) * 2.0 + / TerrainChunkSize::RECT_SIZE.x as f32 + }; + let riverbed_depth = + near_center * water_depth + MIN_DEPTH + waterfall_boost; + // Handle rivers debouching into the ocean nicely by 'flattening' their + // bottom + let riverbed_alt = (water_alt - riverbed_depth) + .max(riverless_alt.min(base_sea_level - MIN_DEPTH)); + alt.with( + min_alt.unwrap_or(riverbed_alt).min(riverbed_alt), + near_center * BANK_STRENGTH, + ) + .with_min(min_alt.unwrap_or(riverbed_alt).min(riverbed_alt)) + } else { + const GORGE: f32 = 0.5; + const BANK_SCALE: f32 = 24.0; + // Weighting of this riverbank on nearby terrain (higher when closer to + // the river). This 'pulls' the riverbank + // toward the river's altitude to make sure that we get a smooth + // transition from normal terrain to the water. + let weight = Lerp::lerp( + BANK_STRENGTH + / (1.0 + + (river_edge_dist as f32 - 3.0).max(0.0) * BANK_STRENGTH + / BANK_SCALE), + 0.0, + power((river_edge_dist / BANK_SCALE).clamped(0.0, 1.0) as f64, 2.0) + as f32, + ); + let alt = alt.with(water_alt + GORGE, weight); + + let alt = if matches!(kind, RiverKind::Ocean) { + alt + } else if (0.0..1.5).contains(&river_edge_dist) + && water_dist.map_or(false, |d| d >= 0.0) + { + alt.with_max(water_alt + GORGE) + } else { + alt + }; + + if matches!(kind, RiverKind::Ocean) { + alt + } else if lake_dist > 0.0 && water_level < unbounded_water_level { + alt.with_max(unbounded_water_level) + } else { + alt + } + } + } else { + alt + } + }, + (_, _) => alt, + }, + ); + let alt = alt + .eval_or(riverless_alt) + .max(if water_dist.map_or(true, |d| d > 0.0) { + // Terrain below sea level breaks things, so force it to never happen + base_sea_level + 0.5 + } else { + f32::MIN + }); + + let riverless_alt_delta = (sim.gen_ctx.small_nz.get( + (wposf_turb.div(200.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))).into_array(), + ) as f32) + .min(1.0) + .max(-1.0) + .abs() + .mul(3.0) + + (sim.gen_ctx.small_nz.get( + (wposf_turb.div(400.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))) + .into_array(), + ) as f32) + .min(1.0) + .max(-1.0) + .abs() + .mul(3.0); // Cliffs let cliff_factor = (alt @@ -282,450 +765,15 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { 0.0 } * (1.0 - near_water * 3.0).max(0.0).powi(2); let cliff_offset = cliff * cliff_height; - let alt = alt + (cliff - 0.5) * cliff_height; + let riverless_alt_delta = riverless_alt_delta + (cliff - 0.5) * cliff_height; - // Find the average distance to each neighboring body of water. - let mut river_count = 0.0f64; - let mut overlap_count = 0.0f64; - let mut river_distance_product = 1.0f64; - let mut river_overlap_distance_product = 0.0f64; - let mut max_river = None; - let mut max_key = None; - // IDEA: - // For every "nearby" chunk, check whether it is a river. If so, find the - // closest point on the river segment to wposf (if two point are - // equidistant, choose the earlier one), calling this point river_pos - // and the length (from 0 to 1) along the river segment for the nearby - // chunk river_t. Let river_dist be the distance from river_pos to wposf. - // - // Let river_alt be the interpolated river height at this point - // (from the alt/water altitude at the river, to the alt/water_altitude of the - // downhill river, increasing with river_t). - // - // Now, if river_dist is <= river_width * 0.5, then we don't care what altitude - // we use, and mark that we are on a river (we decide what river to use - // using a heuristic, and set the solely according to the computed - // river_alt for that point). - // - // Otherwise, we let dist = river_dist - river_width * 0.5. - // - // If dist >= TerrainChunkSize::RECT_SIZE.x, we don't include this river in the - // calculation of the correct altitude for this point. - // - // Otherwise (i.e. dist < TerrainChunkSize::RECT_SIZE.x), we want to bias the - // altitude of this point towards the altitude of the river. - // Specifically, as the dist goes from TerrainChunkSize::RECT_SIZE.x to - // 0, the weighted altitude of this point should go from - // alt to river_alt. - neighbor_river_data.for_each(|(river_chunk_idx, river_chunk, river, dist)| { - match river.river_kind { - Some(kind) => { - if kind.is_river() && dist.is_none() { - // Ostensibly near a river segment, but not "usefully" so (there is no - // closest point between t = 0.0 and t = 1.0). - return; - } else { - let river_dist = dist.map(|(_, dist, _, (river_t, _, downhill_river))| { - let downhill_height = if kind.is_river() { - Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river.alt.max(downhill_river.water_alt), - river_t as f32, - ) as f64 - } else { - let neighbor_pos = - river_chunk_idx.map(|e| e as f64) * neighbor_coef; - -(wposf - neighbor_pos).magnitude() - }; - (Reverse((dist.x, dist.y)), downhill_height) - }); - let river_dist = river_dist.or_else(|| { - if !kind.is_river() { - let neighbor_pos = - river_chunk_idx.map(|e| e as f64) * neighbor_coef; - let dist = (wposf - neighbor_pos).magnitude(); - let dist_upon = - (dist - TerrainChunkSize::RECT_SIZE.x as f64 * 0.5).max(0.0); - let dist_ = if dist == 0.0 { f64::INFINITY } else { -dist }; - Some((Reverse((0.0, dist_upon)), dist_)) - } else { - None - } - }); - let river_key = (river_dist, Reverse(kind)); - if max_key < Some(river_key) { - max_river = Some((river_chunk_idx, river_chunk, river, dist)); - max_key = Some(river_key); - } - } + let warp_factor = water_dist.map_or(1.0, |d| d.max(0.0) / 64.0); - // NOTE: we scale by the distance to the river divided by the difference - // between the edge of the river that we intersect, and the remaining distance - // until the nearest point in "this" chunk (i.e. the one whose top-left corner - // is chunk_pos) that is at least 2 chunks away from the river source. - if let Some((_, dist, _, (river_t, _, downhill_river_chunk))) = dist { - let max_distance = if !river.is_river() { - /*(*/ - TerrainChunkSize::RECT_SIZE.x as f64 /* * (1.0 - (2.0f64.sqrt() / 2.0))) + 4.0*/ - lake_width * 0.5 - } else { - TerrainChunkSize::RECT_SIZE.x as f64 - }; - let scale_factor = max_distance; - let river_dist = dist.y; - - if !(dist.x == 0.0 && river_dist < scale_factor) { - return; - } - // We basically want to project outwards from river_pos, along the current - // tangent line, to chunks <= river_width * 1.0 away from this - // point. We *don't* want to deal with closer chunks because they - - // NOTE: river_width <= 2 * max terrain chunk size width, so this should not - // lead to division by zero. - // NOTE: If distance = 0.0 this goes to zero, which is desired since it - // means points that actually intersect with rivers will not be interpolated - // with the "normal" height of this point. - // NOTE: We keep the maximum at 1.0 so we don't undo work from another river - // just by being far away. - let river_scale = river_dist / scale_factor; - let river_alt = - Lerp::lerp(river_chunk.alt, downhill_river_chunk.alt, river_t as f32); - let river_alt = Lerp::lerp(river_alt, alt, river_scale as f32); - let river_alt_diff = river_alt - alt; - let river_alt_inv = river_alt_diff as f64; - river_overlap_distance_product += (1.0 - river_scale) * river_alt_inv; - overlap_count += 1.0 - river_scale; - river_count += 1.0; - river_distance_product *= river_scale; - } - } - None => {} - } - }); - - let river_scale_factor = if river_count == 0.0 { - 1.0 - } else { - let river_scale_factor = river_distance_product; - if river_scale_factor == 0.0 { - 0.0 - } else { - river_scale_factor.powf(if river_count == 0.0 { - 1.0 - } else { - 1.0 / river_count - }) - } - }; - - let alt_for_river = alt - + if overlap_count == 0.0 { - 0.0 - } else { - river_overlap_distance_product / overlap_count - } as f32; - - let riverless_alt_delta = (sim.gen_ctx.small_nz.get( - (wposf_turb.div(200.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))).into_array(), - ) as f32) - .min(1.0) - .max(-1.0) - .abs() - .mul(3.0) - + (sim.gen_ctx.small_nz.get( - (wposf_turb.div(400.0 * (32.0 / TerrainChunkSize::RECT_SIZE.x as f64))) - .into_array(), - ) as f32) - .min(1.0) - .max(-1.0) - .abs() - .mul(3.0); - - let downhill = sim_chunk.downhill; - let downhill_pos = downhill.and_then(|downhill_pos| sim.get(downhill_pos)); - debug_assert!(sim_chunk.water_alt >= CONFIG.sea_level); - - let downhill_water_alt = downhill_pos - .map(|downhill_chunk| { - downhill_chunk - .water_alt - .min(sim_chunk.water_alt) - .max(sim_chunk.alt.min(sim_chunk.water_alt)) - }) - .unwrap_or(CONFIG.sea_level); - - let river_gouge = 0.5; - let (_in_water, water_dist, alt_, water_level, _riverless_alt, warp_factor) = if let Some( - (max_border_river_pos, river_chunk, max_border_river, max_border_river_dist), - ) = - max_river - { - // This is flowing into a lake, or a lake, or is at least a non-ocean tile. - // - // If we are <= water_alt, we are in the lake; otherwise, we are flowing into - // it. - let (in_water, water_dist, new_alt, new_water_alt, riverless_alt, warp_factor) = - max_border_river - .river_kind - .and_then(|river_kind| { - match river_kind { - RiverKind::River { cross_section } => { - if max_border_river_dist.map(|(_, dist, _, _)| dist) - != Some(Vec2::zero()) - { - return None; - } - let ( - _, - _, - river_width, - (river_t, (river_pos, _), downhill_river_chunk), - ) = max_border_river_dist.unwrap(); - let river_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk.alt.max(downhill_river_chunk.water_alt), - river_t as f32, - ); - let new_alt = river_alt - river_gouge; - let river_dist = wposf.distance(river_pos); - let river_height_factor = river_dist / (river_width * 0.5); - - let valley_alt = Lerp::lerp( - new_alt - cross_section.y.max(1.0), - new_alt - 1.0, - (river_height_factor * river_height_factor) as f32, - ); - - Some(( - true, - Some((river_dist - river_width * 0.5) as f32), - valley_alt, - new_alt, - alt, //river_alt + cross_section.y.max(1.0), - 0.0, - )) - }, - _ => None, - } - }) - .unwrap_or_else(|| { - max_border_river - .river_kind - .map(|river_kind| { - match river_kind { - RiverKind::Ocean => { - let ( - _, - dist, - river_width, - (river_t, (river_pos, _), downhill_river_chunk), - ) = if let Some(dist) = max_border_river_dist { - dist - } else { - error!( - ?max_border_river, - ?chunk_pos, - ?max_border_river_pos, - "downhill error details" - ); - panic!( - "Oceans should definitely have a downhill! \ - ...Right?" - ); - }; - let lake_water_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk - .alt - .max(downhill_river_chunk.water_alt), - river_t as f32, - ); - - if dist == Vec2::zero() { - let river_dist = wposf.distance(river_pos); - let _river_height_factor = - river_dist / (river_width * 0.5); - return ( - true, - Some((river_dist - river_width * 0.5) as f32), - alt_for_river - .min(lake_water_alt - 1.0 - river_gouge), - lake_water_alt - river_gouge, - alt_for_river.max(lake_water_alt), - 0.0, - ); - } - - ( - river_scale_factor <= 1.0, - Some( - (wposf.distance(river_pos) - river_width * 0.5) - as f32, - ), - alt_for_river, - downhill_water_alt, - alt_for_river, - river_scale_factor as f32, - ) - }, - RiverKind::Lake { .. } => { - let lake_dist = (max_border_river_pos.map(|e| e as f64) - * neighbor_coef) - .distance(wposf); - let downhill_river_chunk = max_border_river_pos; - let lake_id_dist = downhill_river_chunk - chunk_pos; - let in_bounds = lake_id_dist.x >= -1 - && lake_id_dist.y >= -1 - && lake_id_dist.x <= 1 - && lake_id_dist.y <= 1; - let in_bounds = in_bounds - && (lake_id_dist.x >= 0 && lake_id_dist.y >= 0); - let (_, dist, _, (river_t, _, downhill_river_chunk)) = - if let Some(dist) = max_border_river_dist { - dist - } else if lake_dist - <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0 - || in_bounds - { - let gouge_factor = 0.0; - return ( - in_bounds - || downhill_water_alt - .max(river_chunk.water_alt) - > alt_for_river, - Some(lake_dist as f32), - alt_for_river, - (downhill_water_alt.max(river_chunk.water_alt) - - river_gouge), - alt_for_river, - river_scale_factor as f32 - * (1.0 - gouge_factor), - ); - } else { - return ( - false, - Some(lake_dist as f32), - alt_for_river, - downhill_water_alt, - alt_for_river, - river_scale_factor as f32, - ); - }; - - let lake_dist = dist.y; - let lake_water_alt = Lerp::lerp( - river_chunk.alt.max(river_chunk.water_alt), - downhill_river_chunk - .alt - .max(downhill_river_chunk.water_alt), - river_t as f32, - ); - if dist == Vec2::zero() { - return ( - true, - Some(lake_dist as f32), - alt_for_river - .min(lake_water_alt - 1.0 - river_gouge), - lake_water_alt - river_gouge, - alt_for_river.max(lake_water_alt), - 0.0, - ); - } - if lake_dist <= TerrainChunkSize::RECT_SIZE.x as f64 * 1.0 - || in_bounds - { - let gouge_factor = if in_bounds && lake_dist <= 1.0 { - 1.0 - } else { - 0.0 - }; - let in_bounds_ = lake_dist - <= TerrainChunkSize::RECT_SIZE.x as f64 * 0.5; - if gouge_factor == 1.0 { - return ( - true, - Some(lake_dist as f32), - alt.min(lake_water_alt - 1.0 - river_gouge), - downhill_water_alt.max(lake_water_alt) - - river_gouge, - alt.max(lake_water_alt), - 0.0, - ); - } else { - return ( - true, - Some(lake_dist as f32), - alt_for_river, - if in_bounds_ { - downhill_water_alt.max(lake_water_alt) - } else { - downhill_water_alt - } - river_gouge, - alt_for_river, - river_scale_factor as f32 - * (1.0 - gouge_factor), - ); - } - } - ( - river_scale_factor <= 1.0, - Some(lake_dist as f32), - alt_for_river, - downhill_water_alt, - alt_for_river, - river_scale_factor as f32, - ) - }, - RiverKind::River { .. } => { - let (_, _, river_width, (_, (river_pos, _), _)) = - max_border_river_dist.unwrap(); - let river_dist = wposf.distance(river_pos); - - // FIXME: Make water altitude accurate. - ( - river_scale_factor <= 1.0, - Some((river_dist - river_width * 0.5) as f32), - alt_for_river, - downhill_water_alt, - alt, //alt_for_river, - river_scale_factor as f32, - ) - }, - } - }) - .unwrap_or(( - false, - None, - alt_for_river, - downhill_water_alt, - alt, //alt_for_river, - river_scale_factor as f32, - )) - }); - ( - in_water, - water_dist, - new_alt, - new_water_alt, - riverless_alt, - warp_factor, - ) - } else { - ( - false, - None, - alt_for_river, - downhill_water_alt, - alt, //alt_for_river, - 1.0, - ) - }; // NOTE: To disable warp, uncomment this line. // let warp_factor = 0.0; let riverless_alt_delta = Lerp::lerp(0.0, riverless_alt_delta, warp_factor); - let riverless_alt = alt + riverless_alt_delta; //riverless_alt + riverless_alt_delta; - let alt = alt_ + riverless_alt_delta; + let alt = alt + riverless_alt_delta; let basement = alt + sim.get_interpolated_monotone(wpos, |chunk| chunk.basement.sub(chunk.alt))?; @@ -982,18 +1030,17 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { .add(((marble - 0.5) / 0.5) * 0.5) .add(((marble_mid - 0.5) / 0.5) * 0.25) .add(((marble_small - 0.5) / 0.5) * 0.175); - let (alt, ground, sub_surface_color, snow_cover) = if snow_cover <= 0.0 && alt > water_level - { + let (alt, ground, sub_surface_color) = if snow_cover <= 0.0 && alt > water_level { // Allow snow cover. ( alt + 1.0 - snow_cover.max(0.0), Rgb::lerp(snow, ground, snow_cover), Lerp::lerp(sub_surface_color, ground, alt.sub(basement).mul(0.15)), - true, ) } else { - (alt, ground, sub_surface_color, false) + (alt, ground, sub_surface_color) }; + let snow_cover = snow_cover <= 0.0; // Make river banks not have grass let ground = water_dist @@ -1004,24 +1051,6 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { // dirt let ground = Lerp::lerp(ground, sub_surface_color, marble_mid * tree_density); - let near_ocean = max_river.and_then(|(_, _, river_data, _)| { - if (river_data.is_lake() || river_data.river_kind == Some(RiverKind::Ocean)) - && alt <= water_level.max(CONFIG.sea_level + 5.0) - { - Some(water_level) - } else { - None - } - }); - - let ocean_level = if let Some(_sea_level) = near_ocean { - alt - CONFIG.sea_level - } else { - 5.0 - }; - - let gradient = sim.get_gradient_approx(chunk_pos); - let path = sim.get_nearest_path(wpos); let cave = sim.get_nearest_cave(wpos); @@ -1035,11 +1064,11 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { surface_color: Rgb::lerp( sub_surface_color, Rgb::lerp( + // Beach Rgb::lerp(cliff, sand, alt.sub(basement).mul(0.25)), // Land ground, - // Beach - ((ocean_level - 1.0) / 2.0).max(0.0), + ((alt - base_sea_level) / 12.0).clamped(0.0, 1.0), ), surface_veg, ), diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 1ed4cc8410..9d3148eb80 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -1,9 +1,12 @@ pub mod scatter; +pub mod shrub; pub mod spot; pub mod tree; pub mod wildlife; -pub use self::{scatter::apply_scatter_to, spot::apply_spots_to, tree::apply_trees_to}; +pub use self::{ + scatter::apply_scatter_to, shrub::apply_shrubs_to, spot::apply_spots_to, tree::apply_trees_to, +}; use crate::{ column::ColumnSample, diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index 382722c643..01fef8e36a 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -9,7 +9,8 @@ fn close(x: f32, tgt: f32, falloff: f32) -> f32 { (1.0 - (x - tgt).abs() / falloff).max(0.0).powf(0.125) } -const MUSH_FACT: f32 = 1.0e-4; // To balance everything around the mushroom spawning rate +const MUSH_FACT: f32 = 1.0e-4; // To balance things around the mushroom spawning rate +const GRASS_FACT: f32 = 1.0e-3; // To balance things around the grass spawning rate const DEPTH_WATER_NORM: f32 = 15.0; // Water depth at which regular underwater sprites start spawning pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { use SpriteKind::*; @@ -18,132 +19,132 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { let scatter: &[( _, bool, - fn(&SimChunk, &ColumnSample) -> (f32, Option<(f32, f32)>), + fn(&SimChunk, &ColumnSample) -> (f32, Option<(f32, f32, f32)>), )] = &[ - // (density, Option<(wavelen, threshold)>) + // (density, Option<(base_density_proportion, wavelen, threshold)>) // Flowers - (BlueFlower, false, |c, col| { + (BlueFlower, false, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.7).min(close( - c.humidity, + close(col.temp, CONFIG.temperate_temp, 0.7).min(close( + col.humidity, CONFIG.jungle_hum, 0.4, )) * col.tree_density * MUSH_FACT * 256.0, - Some((256.0, 0.25)), + Some((0.0, 256.0, 0.25)), ) }), - (PinkFlower, false, |c, col| { + (PinkFlower, false, |_, col| { ( - close(c.temp, 0.0, 0.7).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) + close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.1)), + Some((0.0, 100.0, 0.1)), ) }), - (PurpleFlower, false, |c, col| { + (PurpleFlower, false, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.7).min(close( - c.humidity, + close(col.temp, CONFIG.temperate_temp, 0.7).min(close( + col.humidity, CONFIG.jungle_hum, 0.4, )) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.1)), + Some((0.0, 100.0, 0.1)), ) }), - (RedFlower, false, |c, col| { + (RedFlower, false, |_, col| { ( - close(c.temp, CONFIG.tropical_temp, 0.7).min(close( - c.humidity, + close(col.temp, CONFIG.tropical_temp, 0.7).min(close( + col.humidity, CONFIG.jungle_hum, 0.4, )) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.1)), + Some((0.0, 100.0, 0.1)), ) }), - (WhiteFlower, false, |c, col| { + (WhiteFlower, false, |_, col| { ( - close(c.temp, 0.0, 0.7).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) + close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.1)), + Some((0.0, 100.0, 0.1)), ) }), - (YellowFlower, false, |c, col| { + (YellowFlower, false, |_, col| { ( - close(c.temp, 0.0, 0.7).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) + close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.1)), + Some((0.0, 100.0, 0.1)), ) }), - (Cotton, false, |c, col| { + (Cotton, false, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.7).min(close( - c.humidity, + close(col.temp, CONFIG.temperate_temp, 0.7).min(close( + col.humidity, CONFIG.jungle_hum, 0.4, )) * col.tree_density * MUSH_FACT * 75.0, - Some((256.0, 0.25)), + Some((0.0, 256.0, 0.25)), ) }), - (Sunflower, false, |c, col| { + (Sunflower, false, |_, col| { ( - close(c.temp, 0.0, 0.7).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) + close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) * col.tree_density * MUSH_FACT * 350.0, - Some((100.0, 0.15)), + Some((0.0, 100.0, 0.15)), ) }), - (WildFlax, false, |c, col| { + (WildFlax, false, |_, col| { ( - close(c.temp, 0.0, 0.7).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) + close(col.temp, 0.0, 0.7).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) * col.tree_density * MUSH_FACT * 600.0, - Some((100.0, 0.15)), + Some((0.0, 100.0, 0.15)), ) }), // Herbs and Spices - (LingonBerry, false, |c, _| { + (LingonBerry, false, |_, col| { ( - close(c.temp, 0.3, 0.4).min(close(c.humidity, CONFIG.jungle_hum, 0.5)) + close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.5)) * MUSH_FACT * 2.5, None, ) }), - (LeafyPlant, false, |c, _| { + (LeafyPlant, false, |_, col| { ( - close(c.temp, 0.3, 0.4).min(close(c.humidity, CONFIG.jungle_hum, 0.3)) - * MUSH_FACT + close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.jungle_hum, 0.3)) + * GRASS_FACT * 4.0, None, ) }), - (Fern, false, |c, _| { + (Fern, false, |_, col| { ( - close(c.temp, 0.3, 0.4).min(close(c.humidity, CONFIG.forest_hum, 0.5)) - * MUSH_FACT + close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.forest_hum, 0.5)) + * GRASS_FACT * 0.25, - Some((64.0, 0.2)), + Some((0.0, 64.0, 0.2)), ) }), - (Blueberry, false, |c, _| { + (Blueberry, false, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.5).min(close( - c.humidity, + close(col.temp, CONFIG.temperate_temp, 0.5).min(close( + col.humidity, CONFIG.forest_hum, 0.5, )) * MUSH_FACT @@ -151,191 +152,200 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { None, ) }), - (Pumpkin, false, |c, _| { + (Pumpkin, false, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.5).min(close( - c.humidity, + close(col.temp, CONFIG.temperate_temp, 0.5).min(close( + col.humidity, CONFIG.forest_hum, 0.5, )) * MUSH_FACT * 500.0, - Some((512.0, 0.05)), + Some((0.0, 512.0, 0.05)), ) }), // Collectable Objects // Only spawn twigs in temperate forests - (Twigs, false, |c, _| { + (Twigs, false, |_, col| { ( - (c.tree_density * 1.25 - 0.25).powf(0.5).max(0.0) * 0.75e-3, + (col.tree_density * 1.25 - 0.25).powf(0.5).max(0.0) * 0.75e-3, None, ) }), - (Stones, false, |c, _| { - ((c.rockiness - 0.5).max(0.025) * 1.0e-3, None) + (Stones, false, |chunk, _| { + ((chunk.rockiness - 0.5).max(0.025) * 1.0e-3, None) }), - (Copper, false, |c, _| { - ((c.rockiness - 0.5).max(0.0) * 1.5e-3, None) + (Copper, false, |chunk, _| { + ((chunk.rockiness - 0.5).max(0.0) * 1.5e-3, None) }), - (Tin, false, |c, _| { - ((c.rockiness - 0.5).max(0.0) * 1.5e-3, None) + (Tin, false, |chunk, _| { + ((chunk.rockiness - 0.5).max(0.0) * 1.5e-3, None) }), // Don't spawn Mushrooms in snowy regions - (Mushroom, false, |c, _| { + (Mushroom, false, |_, col| { ( - close(c.temp, 0.3, 0.4).min(close(c.humidity, CONFIG.forest_hum, 0.35)) * MUSH_FACT, + close(col.temp, 0.3, 0.4).min(close(col.humidity, CONFIG.forest_hum, 0.35)) + * MUSH_FACT, None, ) }), // Grass - (ShortGrass, false, |c, _| { + (ShortGrass, false, |_, col| { ( - close(c.temp, 0.2, 0.65).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) * 0.015, - None, + close(col.temp, 0.2, 0.75).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) + * GRASS_FACT + * 150.0, + Some((0.3, 64.0, 0.3)), ) }), - (MediumGrass, false, |c, _| { + (MediumGrass, false, |_, col| { ( - close(c.temp, 0.2, 0.6).min(close(c.humidity, CONFIG.jungle_hum, 0.4)) * 0.012, - None, + close(col.temp, 0.2, 0.6).min(close(col.humidity, CONFIG.jungle_hum, 0.4)) + * GRASS_FACT + * 120.0, + Some((0.3, 64.0, 0.3)), ) }), - (LongGrass, false, |c, _| { + (LongGrass, false, |_, col| { ( - close(c.temp, 0.3, 0.35).min(close(c.humidity, CONFIG.jungle_hum, 0.3)) * 0.15, - Some((48.0, 0.2)), + close(col.temp, 0.3, 0.35).min(close(col.humidity, CONFIG.jungle_hum, 0.3)) + * GRASS_FACT + * 150.0, + Some((0.1, 48.0, 0.3)), ) }), // Jungle Sprites // (LongGrass, false, |c, col| { // ( - // close(c.temp, CONFIG.tropical_temp, 0.4).min(close( - // c.humidity, + // close(col.temp, CONFIG.tropical_temp, 0.4).min(close( + // col.humidity, // CONFIG.jungle_hum, // 0.6, // )) * 0.08, - // Some((60.0, 5.0)), + // Some((0.0, 60.0, 5.0)), // ) // }), /*(WheatGreen, false, |c, col| { ( - close(c.temp, 0.4, 0.2).min(close(c.humidity, CONFIG.forest_hum, 0.1)) + close(col.temp, 0.4, 0.2).min(close(col.humidity, CONFIG.forest_hum, 0.1)) * MUSH_FACT * 0.001, None, ) }),*/ - (GrassSnow, false, |c, _| { + (GrassSnow, false, |_, col| { ( - close(c.temp, CONFIG.snow_temp - 0.2, 0.4).min(close( - c.humidity, + close(col.temp, CONFIG.snow_temp - 0.2, 0.4).min(close( + col.humidity, CONFIG.forest_hum, 0.5, - )) * 0.01, - Some((48.0, 0.2)), + )) * GRASS_FACT + * 100.0, + Some((0.0, 48.0, 0.2)), ) }), - (Moonbell, false, |c, _| { + (Moonbell, false, |_, col| { ( - close(c.temp, CONFIG.snow_temp - 0.2, 0.4).min(close( - c.humidity, + close(col.temp, CONFIG.snow_temp - 0.2, 0.4).min(close( + col.humidity, CONFIG.forest_hum, 0.5, )) * 0.003, - Some((48.0, 0.2)), + Some((0.0, 48.0, 0.2)), ) }), // Savanna Plants - (SavannaGrass, false, |c, _| { + (SavannaGrass, false, |_, col| { ( { - let savanna = close(c.temp, 1.0, 0.4).min(close(c.humidity, 0.2, 0.25)); - let desert = close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)); - (savanna - desert * 5.0).max(0.0) + let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25); + let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1); + (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 250.0 }, - Some((0.5, 0.2)), + Some((0.15, 64.0, 0.2)), ) }), - (TallSavannaGrass, false, |c, _| { + (TallSavannaGrass, false, |_, col| { ( { - let savanna = close(c.temp, 1.0, 0.4).min(close(c.humidity, 0.2, 0.25)); - let desert = close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)); - (savanna - desert * 5.0).max(0.0) + let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25); + let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1); + (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 150.0 }, - Some((12.5, 0.25)), + Some((0.1, 48.0, 0.2)), ) }), - (RedSavannaGrass, false, |c, _| { + (RedSavannaGrass, false, |_, col| { ( { - let savanna = close(c.temp, 1.0, 0.4).min(close(c.humidity, 0.2, 0.25)); - let desert = close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)); - (savanna - desert * 5.0).max(0.0) + let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25); + let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1); + (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 120.0 }, - Some((0.1, 0.1)), + Some((0.15, 48.0, 0.25)), ) }), - (SavannaBush, false, |c, _| { + (SavannaBush, false, |_, col| { ( { - let savanna = close(c.temp, 1.0, 0.4).min(close(c.humidity, 0.2, 0.25)); - let desert = close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)); - (savanna - desert * 5.0).max(0.0) + let savanna = close(col.temp, 1.0, 0.4) * close(col.humidity, 0.2, 0.25); + let desert = close(col.temp, 1.0, 0.25) * close(col.humidity, 0.0, 0.1); + (savanna - desert * 5.0).max(0.0) * GRASS_FACT * 40.0 }, - Some((0.08, 0.05)), + Some((0.1, 96.0, 0.15)), ) }), // Desert Plants - (DeadBush, false, |c, _| { + (DeadBush, false, |_, col| { ( - close(c.temp, 1.0, 0.95).min(close(c.humidity, 0.0, 0.3)) * MUSH_FACT * 7.5, + close(col.temp, 1.0, 0.95).min(close(col.humidity, 0.0, 0.3)) * MUSH_FACT * 7.5, None, ) }), - (Pyrebloom, false, |c, _| { + (Pyrebloom, false, |_, col| { ( - close(c.temp, 1.0, 0.95).min(close(c.humidity, 0.0, 0.3)) * MUSH_FACT * 0.35, + close(col.temp, 1.0, 0.95).min(close(col.humidity, 0.0, 0.3)) * MUSH_FACT * 0.35, None, ) }), - (LargeCactus, false, |c, _| { + (LargeCactus, false, |_, col| { ( - close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)) * MUSH_FACT * 3.5, + close(col.temp, 1.0, 0.25).min(close(col.humidity, 0.0, 0.1)) * MUSH_FACT * 1.5, None, ) }), - (RoundCactus, false, |c, _| { + (RoundCactus, false, |_, col| { ( - close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)) * MUSH_FACT * 5.5, + close(col.temp, 1.0, 0.25).min(close(col.humidity, 0.0, 0.1)) * MUSH_FACT * 2.5, None, ) }), - (ShortCactus, false, |c, _| { + (ShortCactus, false, |_, col| { ( - close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)) * MUSH_FACT * 5.5, + close(col.temp, 1.0, 0.25).min(close(col.humidity, 0.0, 0.1)) * MUSH_FACT * 2.5, None, ) }), - (MedFlatCactus, false, |c, _| { + (MedFlatCactus, false, |_, col| { ( - close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)) * MUSH_FACT * 5.5, + close(col.temp, 1.0, 0.25).min(close(col.humidity, 0.0, 0.1)) * MUSH_FACT * 2.5, None, ) }), - (ShortFlatCactus, false, |c, _| { + (ShortFlatCactus, false, |_, col| { ( - close(c.temp, 1.0, 0.25).min(close(c.humidity, 0.0, 0.1)) * MUSH_FACT * 5.5, + close(col.temp, 1.0, 0.25).min(close(col.humidity, 0.0, 0.1)) * MUSH_FACT * 2.5, None, ) }), - (Reed, false, |c, col| { + (Reed, false, |_, col| { ( - close(c.humidity, CONFIG.jungle_hum, 0.7) + close(col.humidity, CONFIG.jungle_hum, 0.9) * col .water_dist .map(|wd| Lerp::lerp(0.2, 0.0, (wd / 8.0).clamped(0.0, 1.0))) - .unwrap_or(0.0), - Some((128.0, 0.5)), + .unwrap_or(0.0) + * ((col.alt - CONFIG.sea_level) / 12.0).clamped(0.0, 1.0), + Some((0.2, 128.0, 0.5)), ) }), // Underwater chests @@ -374,23 +384,23 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { } else { 0.0 }, - Some((100.0, 0.15)), + Some((0.0, 100.0, 0.15)), ) }), // seagrass - (Seagrass, true, |c, col| { + (Seagrass, true, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.8) + close(col.temp, CONFIG.temperate_temp, 0.8) * MUSH_FACT * 300.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 18.0 { 1.0 } else { 0.0 }, - Some((150.0, 0.3)), + Some((0.0, 150.0, 0.3)), ) }), // seagrass, coastal patches @@ -398,44 +408,44 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { ( MUSH_FACT * 600.0 - * if col.water_level < CONFIG.sea_level && (col.water_level - col.alt) < 3.0 { + * if col.water_level <= CONFIG.sea_level && (col.water_level - col.alt) < 3.0 { 1.0 } else { 0.0 }, - Some((150.0, 0.4)), + Some((0.0, 150.0, 0.4)), ) }), // scattered seaweed (temperate species) - (SeaweedTemperate, true, |c, col| { + (SeaweedTemperate, true, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.8) + close(col.temp, CONFIG.temperate_temp, 0.8) * MUSH_FACT * 50.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 11.0 { 1.0 } else { 0.0 }, - Some((500.0, 0.75)), + Some((0.0, 500.0, 0.75)), ) }), // scattered seaweed (tropical species) - (SeaweedTropical, true, |c, col| { + (SeaweedTropical, true, |_, col| { ( - close(c.temp, 1.0, 0.95) + close(col.temp, 1.0, 0.95) * MUSH_FACT * 50.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 11.0 { 1.0 } else { 0.0 }, - Some((500.0, 0.75)), + Some((0.0, 500.0, 0.75)), ) }), // Caulerpa lentillifera algae patch @@ -443,14 +453,14 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { ( MUSH_FACT * 250.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0 { 1.0 } else { 0.0 }, - Some((100.0, 0.15)), + Some((0.0, 100.0, 0.15)), ) }), // Caulerpa prolifera algae patch @@ -458,110 +468,110 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { ( MUSH_FACT * 250.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0 { 1.0 } else { 0.0 }, - Some((100.0, 0.15)), + Some((0.0, 100.0, 0.15)), ) }), // Mermaids' fan algae patch - (MermaidsFan, true, |c, col| { + (MermaidsFan, true, |_, col| { ( - close(c.temp, 1.0, 0.95) + close(col.temp, 1.0, 0.95) * MUSH_FACT * 500.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0 { 1.0 } else { 0.0 }, - Some((50.0, 0.10)), + Some((0.0, 50.0, 0.10)), ) }), // Sea anemones - (SeaAnemone, true, |c, col| { + (SeaAnemone, true, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.8) + close(col.temp, CONFIG.temperate_temp, 0.8) * MUSH_FACT * 125.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM - 9.0 { 1.0 } else { 0.0 }, - Some((100.0, 0.3)), + Some((0.0, 100.0, 0.3)), ) }), // Giant Kelp - (GiantKelp, true, |c, col| { + (GiantKelp, true, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.8) + close(col.temp, CONFIG.temperate_temp, 0.8) * MUSH_FACT * 220.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM - 9.0 { 1.0 } else { 0.0 }, - Some((200.0, 0.4)), + Some((0.0, 200.0, 0.4)), ) }), // Bull Kelp - (BullKelp, true, |c, col| { + (BullKelp, true, |_, col| { ( - close(c.temp, CONFIG.temperate_temp, 0.7) + close(col.temp, CONFIG.temperate_temp, 0.7) * MUSH_FACT * 300.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 3.0 { 1.0 } else { 0.0 }, - Some((75.0, 0.3)), + Some((0.0, 75.0, 0.3)), ) }), // Stony Corals - (StonyCoral, true, |c, col| { + (StonyCoral, true, |_, col| { ( - close(c.temp, 1.0, 0.9) + close(col.temp, 1.0, 0.9) * MUSH_FACT * 160.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0 { 1.0 } else { 0.0 }, - Some((120.0, 0.4)), + Some((0.0, 120.0, 0.4)), ) }), // Soft Corals - (SoftCoral, true, |c, col| { + (SoftCoral, true, |_, col| { ( - close(c.temp, 1.0, 0.9) + close(col.temp, 1.0, 0.9) * MUSH_FACT * 120.0 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 10.0 { 1.0 } else { 0.0 }, - Some((120.0, 0.4)), + Some((0.0, 120.0, 0.4)), ) }), // Seashells @@ -569,7 +579,7 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { ( (c.rockiness - 0.5).max(0.0) * 1.0e-3 - * if col.water_level < CONFIG.sea_level + * if col.water_level <= CONFIG.sea_level && col.alt < col.water_level - DEPTH_WATER_NORM + 20.0 { 1.0 @@ -594,16 +604,16 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { ]; canvas.foreach_col(|canvas, wpos2d, col| { - let underwater = col.water_level > col.alt; + let underwater = col.water_level.floor() > col.alt; let kind = scatter .iter() .enumerate() .find_map(|(i, (kind, is_underwater, f))| { let (density, patch) = f(canvas.chunk(), col); - let is_patch = patch - .map(|(wavelen, threshold)| { - canvas + let density = patch + .map(|(base_density_prop, wavelen, threshold)| { + if canvas .index() .noise .scatter_nz @@ -614,10 +624,14 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) { ) .abs() > 1.0 - threshold as f64 + { + density + } else { + density * base_density_prop + } }) - .unwrap_or(true); + .unwrap_or(density); if density > 0.0 - && is_patch && rng.gen::() < density //RandomField::new(i as u32).chance(Vec3::new(wpos2d.x, wpos2d.y, 0), density) && underwater == *is_underwater { diff --git a/world/src/layer/shrub.rs b/world/src/layer/shrub.rs new file mode 100644 index 0000000000..6f054354a2 --- /dev/null +++ b/world/src/layer/shrub.rs @@ -0,0 +1,76 @@ +use crate::{ + all::ForestKind, + util::{seed_expan, Sampler, StructureGen2d, UnitChooser}, + Canvas, +}; +use common::{ + assets::AssetHandle, + terrain::structure::{Structure, StructuresGroup}, +}; +use hashbrown::HashMap; +use lazy_static::lazy_static; +use rand::prelude::*; +use rand_chacha::ChaChaRng; +use vek::*; + +lazy_static! { + static ref JUNGLE_SHRUBS: AssetHandle = Structure::load_group("shrubs.jungle"); +} + +struct Shrub { + wpos: Vec3, + seed: u32, + kind: ForestKind, +} + +pub fn apply_shrubs_to(canvas: &mut Canvas, _dynamic_rng: &mut impl Rng) { + let mut shrub_cache = HashMap::new(); + + let shrub_gen = StructureGen2d::new(canvas.index().seed, 8, 4); + + let info = canvas.info(); + canvas.foreach_col(|_, wpos2d, _| { + for (wpos, seed) in std::array::IntoIter::new(shrub_gen.get(wpos2d)) { + shrub_cache.entry(wpos).or_insert_with(|| { + let col = info.col_or_gen(wpos)?; + + let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); + + const BASE_SHRUB_DENSITY: f64 = 0.15; + if rng.gen_bool((BASE_SHRUB_DENSITY * col.tree_density as f64).clamped(0.0, 1.0)) + && col.water_dist.map_or(true, |d| d > 8.0) + && col.alt > col.water_level + && col.spawn_rate > 0.9 + && col.path.map_or(true, |(d, _, _, _)| d > 6.0) + { + Some(Shrub { + wpos: wpos.with_z(col.alt as i32), + seed, + kind: *info + .chunks() + .make_forest_lottery(wpos) + .choose_seeded(seed) + .as_ref()?, + }) + } else { + None + } + }); + } + }); + + for shrub in shrub_cache.values().filter_map(|s| s.as_ref()) { + let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(shrub.seed)); + + let units = UnitChooser::new(shrub.seed).get(shrub.seed).into(); + + let shrubs = match shrub.kind { + ForestKind::Mangrove => &JUNGLE_SHRUBS, + _ => continue, // TODO: Add more shrub varieties + } + .read(); + + let structure = shrubs.choose(&mut rng).unwrap(); + canvas.blit_structure(shrub.wpos, structure, shrub.seed, units, true); + } +} diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index a511b4ffad..05e0f5bc49 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -360,7 +360,7 @@ impl TreeConfig { } pub fn jungle(rng: &mut impl Rng, scale: f32) -> Self { - let scale = scale * (0.9 + rng.gen::().powi(4) * 1.0); + let scale = scale * (0.8 + rng.gen::() * 0.5); let log_scale = 1.0 + scale.log2().max(0.0); Self { @@ -369,14 +369,14 @@ impl TreeConfig { branch_child_len: 0.35, branch_child_radius: 0.5, branch_child_radius_lerp: true, - leaf_radius: 4.0 * log_scale..4.5 * log_scale, - leaf_radius_scaled: 0.0, + leaf_radius: 10.0 * log_scale..11.5 * log_scale, + leaf_radius_scaled: -8.0 * log_scale, straightness: 0.2, max_depth: 2, splits: 7.5..8.5, - split_range: 0.3..1.25, + split_range: 0.2..1.25, branch_len_bias: 0.5, - leaf_vertical_scale: 0.4, + leaf_vertical_scale: 0.35, proportionality: 0.8, inhabited: false, hanging_sprites: &[(0.00007, SpriteKind::Beehive), (0.015, SpriteKind::Liana)], @@ -467,16 +467,16 @@ impl TreeConfig { trunk_len: 13.0 * scale, trunk_radius: 1.65 * scale, branch_child_len: 0.75, - branch_child_radius: 0.75, + branch_child_radius: 0.6, branch_child_radius_lerp: true, - leaf_radius: 2.0 * log_scale..2.1 * log_scale, + leaf_radius: 1.5 * log_scale..2.0 * log_scale, leaf_radius_scaled: 0.0, straightness: 0.3, max_depth: 5, splits: 3.5..4.25, split_range: 0.5..1.25, branch_len_bias: 0.0, - leaf_vertical_scale: 0.5, + leaf_vertical_scale: 0.65, proportionality: 0.5, inhabited: false, hanging_sprites: &[(0.00007, SpriteKind::Beehive)], diff --git a/world/src/lib.rs b/world/src/lib.rs index 2ebd01a561..72edb90318 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -347,6 +347,7 @@ impl World { }; layer::apply_caves_to(&mut canvas, &mut dynamic_rng); + layer::apply_shrubs_to(&mut canvas, &mut dynamic_rng); layer::apply_trees_to(&mut canvas, &mut dynamic_rng); layer::apply_scatter_to(&mut canvas, &mut dynamic_rng); layer::apply_paths_to(&mut canvas); diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 43005634e3..01e9984374 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -1563,8 +1563,10 @@ impl WorldSim { let dist = rpos.map(|e| e as f32).magnitude(); if let Some(c) = self.get_mut(cliff + rpos) { let warp = 1.0 / (1.0 + dist); - c.tree_density *= 1.0 - warp; - c.cliff_height = Lerp::lerp(44.0, 0.0, -1.0 + dist / 3.5); + if !c.river.near_water() { + c.tree_density *= 1.0 - warp; + c.cliff_height = Lerp::lerp(44.0, 0.0, -1.0 + dist / 3.5); + } } }); } @@ -2054,47 +2056,45 @@ impl WorldSim { self.get_nearest_way(wpos, |chunk| Some(chunk.cave)) } + /// Create a [`Lottery>`] that generates [`ForestKind`]s + /// according to the conditions at the given position. If no or fewer + /// trees are appropriate for the conditions, `None` may be generated. + pub fn make_forest_lottery(&self, wpos: Vec2) -> Lottery> { + let chunk = if let Some(chunk) = self.get_wpos(wpos) { + chunk + } else { + return Lottery::from(vec![(1.0, None)]); + }; + let env = chunk.get_environment(); + Lottery::from( + ForestKind::into_enum_iter() + .enumerate() + .map(|(i, fk)| { + const CLUSTER_SIZE: f64 = 48.0; + let nz = (FastNoise2d::new(i as u32 * 37) + .get(wpos.map(|e| e as f64) / CLUSTER_SIZE) + + 1.0) + / 2.0; + (fk.proclivity(&env) * nz, Some(fk)) + }) + .chain(std::iter::once((0.001, None))) + .collect::>(), + ) + } + /// Return an iterator over candidate tree positions (note that only some of /// these will become trees since environmental parameters may forbid /// them spawning). pub fn get_near_trees(&self, wpos: Vec2) -> impl Iterator + '_ { // Deterministic based on wpos let normal_trees = std::array::IntoIter::new(self.gen_ctx.structure_gen.get(wpos)) - .filter_map(move |(pos, seed)| { - let chunk = self.get_wpos(pos)?; - let env = Environment { - humid: chunk.humidity, - temp: chunk.temp, - near_water: if chunk.river.is_lake() - || chunk.river.near_river() - || chunk.alt < CONFIG.sea_level + 6.0 - // Close to sea in altitude - { - 1.0 - } else { - 0.0 - }, - }; + .filter_map(move |(wpos, seed)| { + let lottery = self.make_forest_lottery(wpos); Some(TreeAttr { - pos, + pos: wpos, seed, scale: 1.0, - forest_kind: *Lottery::from( - ForestKind::into_enum_iter() - .enumerate() - .map(|(i, fk)| { - const CLUSTER_SIZE: f64 = 48.0; - let nz = (FastNoise2d::new(i as u32 * 37) - .get(pos.map(|e| e as f64) / CLUSTER_SIZE) - + 1.0) - / 2.0; - (fk.proclivity(&env) * nz, Some(fk)) - }) - .chain(std::iter::once((0.001, None))) - .collect::>(), - ) - .choose_seeded(seed) - .as_ref()?, + forest_kind: *lottery.choose_seeded(seed).as_ref()?, inhabited: false, }) }); @@ -2219,7 +2219,8 @@ impl SimChunk { } else { Some( uniform_idx_as_vec2(map_size_lg, downhill_pre as usize) - * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), + * TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + + TerrainChunkSize::RECT_SIZE.map(|e| e as i32 / 2), ) }; @@ -2295,10 +2296,10 @@ impl SimChunk { .min(1.0); // Add geologically short timescale undulation to the world for various reasons - let alt = alt + let alt = // Don't add undulation to rivers, mainly because this could accidentally result in rivers flowing uphill - + if river.near_water() { - 0.0 + if river.near_water() { + alt } else { // Sand dunes (formed over a short period of time, so we don't care about erosion sim) let warp = Vec2::new( @@ -2323,7 +2324,16 @@ impl SimChunk { const SOIL_SCALE: f32 = 16.0; let soil = soil_nz * SOIL_SCALE * tree_density.sqrt() * humidity.sqrt(); - dune + soil + let warp_factor = ((alt - CONFIG.sea_level) / 16.0).clamped(0.0, 1.0); + + let warp = (dune + soil) * warp_factor; + + // Prevent warping pushing the altitude underwater + if alt + warp < water_alt { + alt + } else { + alt + warp + } }; Self { @@ -2415,4 +2425,20 @@ impl SimChunk { } pub fn near_cliffs(&self) -> bool { self.cliff_height > 0.0 } + + pub fn get_environment(&self) -> Environment { + Environment { + humid: self.humidity, + temp: self.temp, + near_water: if self.river.is_lake() + || self.river.near_river() + || self.alt < CONFIG.sea_level + 6.0 + // Close to sea in altitude + { + 1.0 + } else { + 0.0 + }, + } + } }