use super::{Grid2D, Rng};
use std::f32::consts::PI;
#[derive(Debug, Clone)]
pub struct ClimateParams {
pub equator_temp: f32,
pub polar_temp: f32,
pub lapse_rate: f32,
pub ocean_moderation: f32,
pub prevailing_wind: f32,
pub wind_speed: f32,
pub moisture_rate: f32,
pub orographic_factor: f32,
pub diffusion: f32,
pub sea_level: f32,
}
impl Default for ClimateParams {
fn default() -> Self {
Self {
equator_temp: 30.0,
polar_temp: -20.0,
lapse_rate: 40.0,
ocean_moderation: 0.3,
prevailing_wind: 0.0,
wind_speed: 1.0,
moisture_rate: 0.05,
orographic_factor: 3.0,
diffusion: 0.1,
sea_level: 0.4,
}
}
}
pub fn simulate(heightmap: &Grid2D, iterations: usize, rng: &mut Rng) -> (Grid2D, Grid2D) {
let params = ClimateParams::default();
simulate_with_params(heightmap, iterations, ¶ms, rng)
}
pub fn simulate_with_params(
heightmap: &Grid2D,
iterations: usize,
params: &ClimateParams,
_rng: &mut Rng,
) -> (Grid2D, Grid2D) {
let w = heightmap.width;
let h = heightmap.height;
let mut temperature = Grid2D::new(w, h);
for y in 0..h {
let latitude = (y as f32 / h as f32 - 0.5).abs() * 2.0; let lat_temp = params.equator_temp + (params.polar_temp - params.equator_temp) * latitude;
for x in 0..w {
let elevation = heightmap.get(x, y);
let alt_offset = if elevation > params.sea_level {
-(elevation - params.sea_level) * params.lapse_rate
} else {
0.0
};
let is_ocean = elevation < params.sea_level;
let base = lat_temp + alt_offset;
let temp = if is_ocean {
base * (1.0 - params.ocean_moderation) + lat_temp * params.ocean_moderation
} else {
base
};
temperature.set(x, y, temp);
}
}
for _ in 0..iterations {
diffuse_temperature(&mut temperature, params.diffusion);
}
let mut precipitation = Grid2D::new(w, h);
let wind_dx = params.prevailing_wind.cos();
let wind_dy = params.prevailing_wind.sin();
for wind_band in 0..3 {
let band_angle = match wind_band {
0 => params.prevailing_wind, 1 => params.prevailing_wind + PI, _ => params.prevailing_wind + PI * 0.5, };
let wdx = band_angle.cos() * params.wind_speed;
let wdy = band_angle.sin() * params.wind_speed;
let (y_min, y_max) = match wind_band {
0 => (h / 3, 2 * h / 3), 1 => (0, h / 3), _ => (2 * h / 3, h), };
let mut moisture = Grid2D::new(w, h);
for step in 0..w.max(h) {
for y in y_min..y_max {
for x in 0..w {
let sx = (x as f32 - wdx * step as f32).rem_euclid(w as f32) as usize;
let sy = (y as f32 - wdy * step as f32).clamp(0.0, h as f32 - 1.0) as usize;
let elev = heightmap.get(x, y);
let is_ocean = elev < params.sea_level;
if is_ocean {
moisture.add(x, y, params.moisture_rate);
} else {
let (gx, gy) = heightmap.gradient(x, y);
let upslope = gx * wdx + gy * wdy;
if upslope > 0.0 {
let rain = moisture.get(x, y) * upslope * params.orographic_factor;
let rain = rain.min(moisture.get(x, y));
precipitation.add(x, y, rain);
moisture.add(x, y, -rain);
}
}
}
}
}
for y in y_min..y_max {
for x in 0..w {
precipitation.add(x, y, moisture.get(x, y) * 0.1);
}
}
}
for y in 0..h {
let lat = (y as f32 / h as f32 - 0.5).abs() * 2.0;
let itcz = (1.0 - lat * 3.0).max(0.0); for x in 0..w {
precipitation.add(x, y, itcz * 0.5);
}
}
precipitation.normalize();
(temperature, precipitation)
}
fn diffuse_temperature(temp: &mut Grid2D, rate: f32) {
let w = temp.width;
let h = temp.height;
let old = temp.data.clone();
for y in 1..h - 1 {
for x in 1..w - 1 {
let idx = y * w + x;
let laplacian = old[idx - 1] + old[idx + 1]
+ old[idx - w] + old[idx + w]
- 4.0 * old[idx];
temp.data[idx] += laplacian * rate;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_temperature_latitude() {
let hm = Grid2D::filled(32, 32, 0.5); let mut rng = Rng::new(42);
let (temp, _) = simulate(&hm, 10, &mut rng);
let equator_t = temp.get(16, 16);
let pole_t = temp.get(16, 0);
assert!(equator_t > pole_t, "equator should be warmer: eq={equator_t}, pole={pole_t}");
}
#[test]
fn test_precipitation_exists() {
let hm = Grid2D::filled(32, 32, 0.3); let mut rng = Rng::new(42);
let (_, precip) = simulate(&hm, 10, &mut rng);
let max_p = precip.max_value();
assert!(max_p > 0.0, "should have some precipitation");
}
}