use bevy::prelude::*;
use super::weather::Weather;
use crate::celestial::{SunState, WorldClock};
const NEW_SNOW_DENSITY: f32 = 100.0;
const SETTLED_DENSITY: f32 = 300.0;
const SETTLE_DAYS: f32 = 3.0;
const DEGREE_DAY_FACTOR: f32 = 4.0;
const WET_PER_MM: f32 = 0.5;
const DRY_RATE_H: f32 = 0.05;
const FULL_COVER_DEPTH_M: f32 = 0.05;
const MAX_STEP_H: f64 = 24.0;
#[derive(Resource, Clone, Copy, Debug, Default, Reflect)]
pub struct SurfaceWeathering {
pub wetness: f32,
pub snow_cover: f32,
pub snow_depth_m: f32,
snow_water_mm: f32,
snow_density: f32,
last_hours: Option<f64>,
}
impl SurfaceWeathering {
#[cfg(test)]
pub(crate) fn snow_water_mm(&self) -> f32 {
self.snow_water_mm
}
pub(crate) fn step(&mut self, weather: &Weather, sun_factor: f32, hours: f32) {
if hours <= 0.0 {
return;
}
let snow_fraction = weather.snow_fraction();
let t = weather.temperature_c;
let new_swe = weather.precipitation_mm_h * snow_fraction * hours;
let old_depth = self.snow_depth_m;
self.snow_water_mm += new_swe;
if self.snow_water_mm > 0.0 {
let added_depth = new_swe / NEW_SNOW_DENSITY;
let density = self.snow_water_mm / (old_depth + added_depth).max(1e-6);
let k = 1.0 - (-hours / (SETTLE_DAYS * 24.0)).exp();
self.snow_density = density + (SETTLED_DENSITY - density) * k;
let melt = DEGREE_DAY_FACTOR * t.max(0.0) * hours / 24.0;
self.snow_water_mm = (self.snow_water_mm - melt).max(0.0);
}
self.snow_depth_m = if self.snow_water_mm > 0.0 {
self.snow_water_mm / self.snow_density.max(NEW_SNOW_DENSITY)
} else {
self.snow_density = NEW_SNOW_DENSITY;
0.0
};
let c = (self.snow_depth_m / FULL_COVER_DEPTH_M).clamp(0.0, 1.0);
self.snow_cover = c * c * (3.0 - 2.0 * c);
let rain = weather.precipitation_mm_h * (1.0 - snow_fraction);
let drying = DRY_RATE_H
* (1.0 + weather.wind_speed_ms / 5.0)
* (1.0 + t.max(0.0) / 20.0)
* (1.0 - weather.relative_humidity).max(0.0)
* (1.0 + sun_factor);
self.wetness = (self.wetness + (rain * WET_PER_MM - drying) * hours).clamp(0.0, 1.0);
}
}
pub(crate) fn update_weathering(
clock: Res<WorldClock>,
weather: Res<Weather>,
sun: Res<SunState>,
mut ground: ResMut<SurfaceWeathering>,
) {
let now = clock.hours_since_2000();
let hours = match ground.last_hours {
Some(last) => (now - last).clamp(0.0, MAX_STEP_H) as f32,
None => 0.0,
};
ground.last_hours = Some(now);
let sun_factor = sun.altitude_deg.to_radians().sin().max(0.0);
ground.step(&weather, sun_factor, hours);
}
#[cfg(test)]
mod tests {
use super::*;
fn weather(mm_h: f32, t: f32) -> Weather {
Weather {
precipitation_mm_h: mm_h,
temperature_c: t,
relative_humidity: 0.5,
wind_speed_ms: 0.0,
..Weather::default()
}
}
#[test]
fn ten_hours_of_snow_at_1_5_mm_h_lays_about_15_cm() {
let mut g = SurfaceWeathering::default();
for _ in 0..600 {
g.step(&weather(1.5, -5.0), 0.0, 1.0 / 60.0);
}
assert!((g.snow_water_mm - 15.0).abs() < 1e-3);
assert!(
(0.12..=0.15).contains(&g.snow_depth_m),
"{}",
g.snow_depth_m
);
assert!(g.snow_cover > 0.99);
}
#[test]
fn warm_clear_days_melt_the_pack_away() {
let mut g = SurfaceWeathering::default();
g.step(&weather(1.5, -5.0), 0.0, 10.0);
for _ in 0..48 {
g.step(&weather(0.0, 10.0), 1.0, 0.25);
}
assert_eq!(g.snow_depth_m, 0.0);
assert_eq!(g.snow_cover, 0.0);
}
#[test]
fn an_hour_of_rain_soaks_and_a_warm_dry_afternoon_dries() {
let mut g = SurfaceWeathering::default();
g.step(&weather(4.0, 15.0), 0.0, 1.0);
assert_eq!(g.wetness, 1.0);
g.step(&weather(0.0, 20.0), 1.0, 9.0);
assert!((g.wetness - 0.1).abs() < 1e-4, "{}", g.wetness);
}
}