use glam::Vec3;
use crate::noise::Noise2;
use crate::palette::{self, Palette, ramp, vary};
use super::WorldParams;
use super::zones::{KINDS, NK, ZoneField, ZoneKind, ground_ramp};
pub struct WorldModel {
pub p: WorldParams,
pub pal: Palette,
pub n: Noise2,
pub zones: ZoneField,
pub pois: Vec<super::poi::PoiSite>,
pub network: super::network::Network,
pub erosion: Option<super::erosion::ErosionField>,
pub nx: u32,
pub nz: u32,
pub size_x: f32,
pub size_z: f32,
pub amp: f32,
}
fn soft_steps(h: f32, step: f32) -> f32 {
if step <= 1e-6 {
return h;
}
let q = (h / step).floor() * step;
q + ((h - q) / step).powi(3) * step
}
impl WorldModel {
pub fn new(p: &WorldParams) -> Result<Self, String> {
if !palette::PALETTES.contains(&p.palette.as_str()) {
return Err(format!(
"unknown palette '{}' (available: {})",
p.palette,
palette::PALETTES.join(", ")
));
}
let mut p = p.clone();
p.chunk_size = p.chunk_size.clamp(32.0, 1024.0).round();
p.chunk_resolution = p.chunk_resolution.clamp(16, 512);
let n_chunks = (p.size / p.chunk_size).round().clamp(1.0, 64.0) as u32;
let size = n_chunks as f32 * p.chunk_size;
let specs = p.pois.clone();
let mut model = Self {
pal: palette::by_name(&p.palette),
n: Noise2::new(p.seed),
zones: ZoneField::new(p.seed, &p.zones, p.zone_size),
pois: Vec::new(),
network: super::network::Network::empty(),
erosion: None,
nx: n_chunks,
nz: n_chunks,
size_x: size,
size_z: size,
amp: 70.0 * p.mountainousness.clamp(0.05, 3.0),
p,
};
if model.p.erosion > 0.0 {
let size = model.size_x;
let step = (size / 384.0).clamp(8.0, 24.0);
let n = (size / step).ceil() as i32 + 1;
let mut grid = vec![0.0f32; (n * n) as usize];
for jz in 0..n {
for jx in 0..n {
grid[(jz * n + jx) as usize] =
model.height(jx as f32 * step - size * 0.5, jz as f32 * step - size * 0.5);
}
}
let before = grid.clone();
super::erosion::erode_global(
&mut grid,
n as usize,
model.p.erosion.clamp(0.0, 1.0),
model.p.seed,
);
let delta: Vec<f32> = grid.iter().zip(&before).map(|(a, b)| a - b).collect();
model.erosion = Some(super::erosion::ErosionField {
step,
n,
half: size * 0.5,
delta,
});
}
let km2 = (model.size_x / 1000.0) * (model.size_z / 1000.0);
let n_rivers = model
.p
.rivers
.unwrap_or(((km2 / 6.0).ceil() as u32).min(12));
model.network = super::network::build_rivers(&model, n_rivers);
model.pois = super::poi::place(&model, specs.as_deref());
model.network = super::network::with_roads(&model);
Ok(model)
}
pub fn chunk_origin(&self, cx: u32, cz: u32) -> (f32, f32) {
(
(cx as f32 + 0.5) * self.p.chunk_size - self.size_x * 0.5,
(cz as f32 + 0.5) * self.p.chunk_size - self.size_z * 0.5,
)
}
pub fn height(&self, wx: f32, wz: f32) -> f32 {
let zw = self.zones.weights(wx, wz);
self.height_with_weights(wx, wz, &zw)
}
pub fn height_with_weights(&self, wx: f32, wz: f32, zw: &[f32; NK]) -> f32 {
let n = &self.n;
let amp = self.amp;
let sea = self.p.sea_level;
let cont = n.fbm(wx / 2400.0, wz / 2400.0, 4, 2.0, 0.5);
let hills = n.warped_fbm(wx / 420.0 + 7.3, wz / 420.0 - 3.1, 5, 0.8);
let ridge = (n.ridged(wx / 900.0 + 13.7, wz / 900.0 + 4.2, 5) - 0.35).max(0.0);
let detail = n.fbm(wx / 60.0 + 31.0, wz / 60.0 + 17.0, 3, 2.0, 0.5);
let dune = 1.0 - n.sample(wx / 95.0 + wz / 30.0, wz / 260.0).abs();
let mesa = n.fbm(wx / 520.0 + 87.0, wz / 520.0 + 13.0, 3, 2.0, 0.5);
let zi = |k: ZoneKind| zw[k.index()];
let mut h = sea + cont * amp * 0.10 + detail * 2.5;
let crag = (n.ridged(wx / 210.0 + 51.3, wz / 210.0 + 9.8, 4) - 0.40).max(0.0);
let micro = n.fbm(wx / 13.0 + 7.0, wz / 13.0 + 3.0, 3, 2.0, 0.5);
h += zi(ZoneKind::Mountains)
* (amp * 0.45
+ hills * amp * 0.25
+ ridge * amp * 1.55
+ crag * amp * 0.45 * (0.35 + ridge)
+ cont * amp * 0.20);
h += zi(ZoneKind::Forest) * (10.0 + hills * amp * 0.26 + cont * amp * 0.10);
h += zi(ZoneKind::Plains) * (7.0 + hills * amp * 0.09 + cont * amp * 0.05);
h += zi(ZoneKind::Desert) * (9.0 + dune * amp * 0.16 + hills * amp * 0.05);
h += zi(ZoneKind::Swamp) * (1.6 + hills * 1.6 + detail * 1.4);
h += zi(ZoneKind::Lake) * (-14.0 + hills * 2.0 - cont * 3.0);
h += zi(ZoneKind::Coast) * (2.0 + hills * amp * 0.05 + cont * amp * 0.08);
h += zi(ZoneKind::Badlands)
* (10.0
+ soft_steps(
((mesa + 0.35) * 1.4).clamp(0.0, 1.3) * amp * 0.45,
amp * 0.11,
));
h += micro
* (0.7
+ (zi(ZoneKind::Mountains) * (1.0 + ridge)) * 3.2
+ zi(ZoneKind::Badlands) * 1.6);
let d = h - sea;
let w = 3.0;
let mut h = if d.abs() < w {
sea + w * d.signum() * (d.abs() / w).powf(0.6)
} else {
h
};
if let Some(e) = &self.erosion {
h += e.sample(wx, wz);
}
for s in &self.pois {
if matches!(s.kind, super::poi::PoiKind::Dungeon) {
continue;
}
let (dx, dz) = (wx - s.x, wz - s.z);
let r_out = s.radius * 1.7;
let d2 = dx * dx + dz * dz;
if d2 < r_out * r_out {
let t = ((r_out - d2.sqrt()) / (r_out - s.radius)).clamp(0.0, 1.0);
let t = t * t * (3.0 - 2.0 * t);
h += (s.ground - h) * t * 0.96;
}
}
self.network.apply_height(wx, wz, h)
}
pub fn color(&self, wx: f32, wz: f32, h: f32, slope: f32) -> Vec3 {
let zw = self.zones.weights(wx, wz);
let pal = &self.pal;
let sea = self.p.sea_level;
let t = ((h - sea) / (self.amp * 3.5)).clamp(0.0, 1.0).powf(0.5);
let mut c = Vec3::ZERO;
for (i, k) in KINDS.iter().enumerate() {
if zw[i] > 1e-4 {
c += ramp(&ground_ramp(*k), t) * zw[i];
}
}
let rockiness = ((slope - 0.85) * 1.8).clamp(0.0, 1.0);
c = c * (1.0 - rockiness * 0.85) + pal.rock[1] * 0.9 * rockiness * 0.85;
let snow = ((h - sea - self.amp * 2.1) / (self.amp * 0.5)).clamp(0.0, 1.0)
* (1.0 - rockiness * 0.6)
* (zw[ZoneKind::Mountains.index()] * 1.6).clamp(0.0, 1.0);
c = c * (1.0 - snow) + pal.terrain[5] * snow;
if rockiness > 0.05 {
let band =
((h * 0.55 + self.n.sample(wx * 0.02 + 9.0, wz * 0.02) * 6.0).sin()) * 0.5 + 0.5;
let strata = crate::palette::lerp(pal.rock[1] * 0.8, pal.rock[0] * 1.08, band);
c = crate::palette::lerp(c, strata, rockiness * 0.55);
}
let grassy = zw[ZoneKind::Plains.index()]
+ zw[ZoneKind::Forest.index()]
+ zw[ZoneKind::Lake.index()]
+ zw[ZoneKind::Coast.index()];
if grassy > 0.3 && rockiness < 0.3 {
let patch =
(self.n.fbm(wx / 23.0 + 5.0, wz / 23.0 - 2.0, 2, 2.0, 0.5) * 1.6).clamp(0.0, 1.0);
c = crate::palette::lerp(c, c * Vec3::new(1.16, 1.06, 0.70), patch * grassy * 0.45);
}
let desert = zw[ZoneKind::Desert.index()];
if desert > 0.2 {
let ripple = self.n.sample(wx / 7.0 + wz / 2.3, wz / 34.0);
c *= 1.0 + ripple * 0.06 * desert;
}
let foam = (1.0 - ((h - sea - 0.12) / 0.16).abs()).clamp(0.0, 1.0)
* (1.0 - (slope / 0.25).min(1.0));
if foam > 0.0 {
let fnz = (self.n.sample(wx * 0.9 + 3.0, wz * 0.9) - 0.15).max(0.0);
c = crate::palette::lerp(c, Vec3::splat(0.82), foam * fnz * 0.28);
}
let shore = (1.0 - ((h - sea).abs() / 2.2)).clamp(0.0, 1.0);
c = c * (1.0 - shore * 0.55) + pal.terrain[0] * shore * 0.55;
let rm = self.network.road_mask(wx, wz);
if rm > 0.0 {
c = crate::palette::lerp(c, pal.trunk * 0.8, rm * 0.7);
}
for s in &self.pois {
if matches!(
s.kind,
super::poi::PoiKind::City
| super::poi::PoiKind::Village
| super::poi::PoiKind::Castle
) {
let (dx, dz) = (wx - s.x, wz - s.z);
let r_out = s.radius * 1.15;
let d2 = dx * dx + dz * dz;
if d2 < r_out * r_out {
let t = ((r_out - d2.sqrt()) / (r_out * 0.35)).clamp(0.0, 1.0);
c = crate::palette::lerp(c, pal.trunk * 0.85, t * 0.55);
}
}
}
vary(
c,
0.10,
self.n.sample(wx * 0.13 + 31.0, wz * 0.13 + 17.0) * 0.5 + 0.5,
)
}
pub fn chunk_res(&self, cx: u32, cz: u32) -> u32 {
let base = self.p.chunk_resolution;
if !self.p.adaptive_resolution {
return base;
}
let (ox, oz) = self.chunk_origin(cx, cz);
let cs = self.p.chunk_size;
let near_poi = self.pois.iter().any(|s| {
(s.x - ox).abs() < cs * 0.5 + s.radius * 1.7
&& (s.z - oz).abs() < cs * 0.5 + s.radius * 1.7
});
let (mut lo, mut hi) = (f32::INFINITY, f32::NEG_INFINITY);
let mut steep = 0.0f32;
let mut prev = 0.0;
for jz in 0..=8 {
for jx in 0..=8 {
let h = self.height(
ox + (jx as f32 / 8.0 - 0.5) * cs,
oz + (jz as f32 / 8.0 - 0.5) * cs,
);
lo = lo.min(h);
hi = hi.max(h);
if jx > 0 {
steep = steep.max((h - prev).abs() / (cs / 8.0));
}
prev = h;
}
}
let relief = hi - lo;
let has_net =
self.network.river_within(ox, oz, cs * 0.75) || self.network.road_mask(ox, oz) > 0.0;
if near_poi || relief > self.amp * 0.8 || steep > 0.6 {
(base * 2).min(256)
} else if relief < self.amp * 0.10 && steep < 0.12 && !has_net {
(base / 2).max(32)
} else {
base
}
}
pub fn chunk_seed(&self, cx: u32, cz: u32) -> u64 {
self.p
.seed
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add((cx as u64) << 32 | cz as u64)
.wrapping_mul(0xBF58_476D_1CE4_E5B9)
}
}