veloren_server/weather/
sim.rs

1use common::{
2    grid::Grid,
3    resources::TimeOfDay,
4    weather::{CELL_SIZE, CHUNKS_PER_CELL, Weather, WeatherGrid},
5};
6use noise::{NoiseFn, Perlin, SuperSimplex, Turbulence};
7use vek::*;
8use world::World;
9
10use crate::weather::WEATHER_DT;
11
12fn cell_to_wpos_center(p: Vec2<i32>) -> Vec2<i32> { p * CELL_SIZE as i32 + CELL_SIZE as i32 / 2 }
13
14#[derive(Clone)]
15struct WeatherZone {
16    weather: Weather,
17    /// Time, in seconds this zone lives.
18    time_to_live: f32,
19}
20
21struct CellConsts {
22    humidity: f32,
23}
24
25pub struct WeatherSim {
26    size: Vec2<u32>,
27    consts: Grid<CellConsts>,
28    zones: Grid<Option<WeatherZone>>,
29}
30
31/// A list of weather cells where lightning has a chance to strike.
32#[derive(Default)]
33pub struct LightningCells {
34    pub cells: Vec<Vec2<i32>>,
35}
36
37impl WeatherSim {
38    pub fn new(size: Vec2<u32>, world: &World) -> Self {
39        Self {
40            size,
41            consts: Grid::from_raw(
42                size.as_(),
43                (0..size.x * size.y)
44                    .map(|i| Vec2::new(i % size.x, i / size.x))
45                    .map(|p| {
46                        let mut humid_sum = 0.0;
47
48                        for y in 0..CHUNKS_PER_CELL {
49                            for x in 0..CHUNKS_PER_CELL {
50                                let chunk_pos = p * CHUNKS_PER_CELL + Vec2::new(x, y);
51                                if let Some(chunk) = world.sim().get(chunk_pos.as_()) {
52                                    let env = chunk.get_environment();
53                                    humid_sum += env.humid;
54                                }
55                            }
56                        }
57                        let average_humid = humid_sum / (CHUNKS_PER_CELL * CHUNKS_PER_CELL) as f32;
58                        CellConsts {
59                            humidity: average_humid.powf(0.2).min(1.0),
60                        }
61                    })
62                    .collect::<Vec<_>>(),
63            ),
64            zones: Grid::new(size.as_(), None),
65        }
66    }
67
68    /// Adds a weather zone as a circle at a position, with a given radius. Both
69    /// of which should be in weather cell units
70    pub fn add_zone(&mut self, weather: Weather, pos: Vec2<f32>, radius: f32, time: f32) {
71        let min: Vec2<i32> = (pos - radius).as_::<i32>().map(|e| e.max(0));
72        let max: Vec2<i32> = (pos + radius)
73            .ceil()
74            .as_::<i32>()
75            .map2(self.size.as_::<i32>(), |a, b| a.min(b));
76        for y in min.y..max.y {
77            for x in min.x..max.x {
78                let ipos = Vec2::new(x, y);
79                let p = ipos.as_::<f32>();
80
81                if p.distance_squared(pos) < radius.powi(2) {
82                    self.zones[ipos] = Some(WeatherZone {
83                        weather,
84                        time_to_live: time,
85                    });
86                }
87            }
88        }
89    }
90
91    // Time step is cell size / maximum wind speed.
92    pub fn tick(&mut self, time_of_day: TimeOfDay, out: &mut WeatherGrid) -> LightningCells {
93        let time = time_of_day.0;
94
95        let base_nz: Turbulence<Turbulence<SuperSimplex, Perlin>, Perlin> = Turbulence::new(
96            Turbulence::new(SuperSimplex::new(0))
97                .set_frequency(0.2)
98                .set_power(1.5),
99        )
100        .set_frequency(2.0)
101        .set_power(0.2);
102
103        let rain_nz = SuperSimplex::new(0);
104
105        let mut lightning_cells = Vec::new();
106        for (point, cell) in out.iter_mut() {
107            if let Some(zone) = &mut self.zones[point] {
108                *cell = zone.weather;
109                zone.time_to_live -= WEATHER_DT;
110                if zone.time_to_live <= 0.0 {
111                    self.zones[point] = None;
112                }
113            } else {
114                let wpos = cell_to_wpos_center(point);
115
116                let pos = wpos.as_::<f64>() + time * 0.1;
117
118                let space_scale = 7_500.0;
119                let time_scale = 100_000.0;
120                let spos = (pos / space_scale).with_z(time / time_scale);
121
122                let avg_scale = 30_000.0;
123                let avg_delay = 250_000.0;
124                let pressure = ((base_nz
125                    .get((pos / avg_scale).with_z(time / avg_delay).into_array())
126                    + base_nz.get(
127                        (pos / (avg_scale * 0.25))
128                            .with_z(time / (avg_delay * 0.25))
129                            .into_array(),
130                    ) * 0.5)
131                    * 0.5
132                    + 1.0)
133                    .clamped(0.0, 1.0) as f32
134                    + 0.55
135                    - self.consts[point].humidity * 0.6;
136
137                const RAIN_CLOUD_THRESHOLD: f32 = 0.25;
138                cell.cloud = (1.0 - pressure).max(0.0).powi(2) * 4.0;
139                cell.rain = ((1.0 - pressure - RAIN_CLOUD_THRESHOLD).max(0.0)
140                    * self.consts[point].humidity
141                    * 2.5)
142                    .powf(0.75);
143                cell.wind = Vec2::new(
144                    rain_nz.get(spos.into_array()).powi(3) as f32,
145                    rain_nz.get((spos + 1.0).into_array()).powi(3) as f32,
146                ) * 200.0
147                    * (1.0 - pressure);
148            }
149
150            if cell.rain > 0.2 && cell.cloud > 0.15 {
151                lightning_cells.push(point);
152            }
153        }
154        LightningCells {
155            cells: lightning_cells,
156        }
157    }
158
159    pub fn size(&self) -> Vec2<u32> { self.size }
160}