1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
use common::{
    grid::Grid,
    resources::TimeOfDay,
    weather::{Weather, WeatherGrid, CELL_SIZE, CHUNKS_PER_CELL},
};
use noise::{NoiseFn, Perlin, SuperSimplex, Turbulence};
use vek::*;
use world::World;

use crate::weather::WEATHER_DT;

fn cell_to_wpos_center(p: Vec2<i32>) -> Vec2<i32> { p * CELL_SIZE as i32 + CELL_SIZE as i32 / 2 }

#[derive(Clone)]
struct WeatherZone {
    weather: Weather,
    /// Time, in seconds this zone lives.
    time_to_live: f32,
}

struct CellConsts {
    humidity: f32,
}

pub struct WeatherSim {
    size: Vec2<u32>,
    consts: Grid<CellConsts>,
    zones: Grid<Option<WeatherZone>>,
}

/// A list of weather cells where lightning has a chance to strike.
#[derive(Default)]
pub struct LightningCells {
    pub cells: Vec<Vec2<i32>>,
}

impl WeatherSim {
    pub fn new(size: Vec2<u32>, world: &World) -> Self {
        Self {
            size,
            consts: Grid::from_raw(
                size.as_(),
                (0..size.x * size.y)
                    .map(|i| Vec2::new(i % size.x, i / size.x))
                    .map(|p| {
                        let mut humid_sum = 0.0;

                        for y in 0..CHUNKS_PER_CELL {
                            for x in 0..CHUNKS_PER_CELL {
                                let chunk_pos = p * CHUNKS_PER_CELL + Vec2::new(x, y);
                                if let Some(chunk) = world.sim().get(chunk_pos.as_()) {
                                    let env = chunk.get_environment();
                                    humid_sum += env.humid;
                                }
                            }
                        }
                        let average_humid = humid_sum / (CHUNKS_PER_CELL * CHUNKS_PER_CELL) as f32;
                        CellConsts {
                            humidity: average_humid.powf(0.2).min(1.0),
                        }
                    })
                    .collect::<Vec<_>>(),
            ),
            zones: Grid::new(size.as_(), None),
        }
    }

    /// Adds a weather zone as a circle at a position, with a given radius. Both
    /// of which should be in weather cell units
    pub fn add_zone(&mut self, weather: Weather, pos: Vec2<f32>, radius: f32, time: f32) {
        let min: Vec2<i32> = (pos - radius).as_::<i32>().map(|e| e.max(0));
        let max: Vec2<i32> = (pos + radius)
            .ceil()
            .as_::<i32>()
            .map2(self.size.as_::<i32>(), |a, b| a.min(b));
        for y in min.y..max.y {
            for x in min.x..max.x {
                let ipos = Vec2::new(x, y);
                let p = ipos.as_::<f32>();

                if p.distance_squared(pos) < radius.powi(2) {
                    self.zones[ipos] = Some(WeatherZone {
                        weather,
                        time_to_live: time,
                    });
                }
            }
        }
    }

    // Time step is cell size / maximum wind speed.
    pub fn tick(&mut self, time_of_day: TimeOfDay, out: &mut WeatherGrid) -> LightningCells {
        let time = time_of_day.0;

        let base_nz: Turbulence<Turbulence<SuperSimplex, Perlin>, Perlin> = Turbulence::new(
            Turbulence::new(SuperSimplex::new(0))
                .set_frequency(0.2)
                .set_power(1.5),
        )
        .set_frequency(2.0)
        .set_power(0.2);

        let rain_nz = SuperSimplex::new(0);

        let mut lightning_cells = Vec::new();
        for (point, cell) in out.iter_mut() {
            if let Some(zone) = &mut self.zones[point] {
                *cell = zone.weather;
                zone.time_to_live -= WEATHER_DT;
                if zone.time_to_live <= 0.0 {
                    self.zones[point] = None;
                }
            } else {
                let wpos = cell_to_wpos_center(point);

                let pos = wpos.as_::<f64>() + time * 0.1;

                let space_scale = 7_500.0;
                let time_scale = 100_000.0;
                let spos = (pos / space_scale).with_z(time / time_scale);

                let avg_scale = 30_000.0;
                let avg_delay = 250_000.0;
                let pressure = ((base_nz
                    .get((pos / avg_scale).with_z(time / avg_delay).into_array())
                    + base_nz.get(
                        (pos / (avg_scale * 0.25))
                            .with_z(time / (avg_delay * 0.25))
                            .into_array(),
                    ) * 0.5)
                    * 0.5
                    + 1.0)
                    .clamped(0.0, 1.0) as f32
                    + 0.55
                    - self.consts[point].humidity * 0.6;

                const RAIN_CLOUD_THRESHOLD: f32 = 0.25;
                cell.cloud = (1.0 - pressure).max(0.0).powi(2) * 4.0;
                cell.rain = ((1.0 - pressure - RAIN_CLOUD_THRESHOLD).max(0.0)
                    * self.consts[point].humidity
                    * 2.5)
                    .powf(0.75);
                cell.wind = Vec2::new(
                    rain_nz.get(spos.into_array()).powi(3) as f32,
                    rain_nz.get((spos + 1.0).into_array()).powi(3) as f32,
                ) * 200.0
                    * (1.0 - pressure);
            }

            if cell.rain > 0.2 && cell.cloud > 0.15 {
                lightning_cells.push(point);
            }
        }
        LightningCells {
            cells: lightning_cells,
        }
    }

    pub fn size(&self) -> Vec2<u32> { self.size }
}