Skip to main content

banished_game/nature/
environment.rs

1//! The environment: the temperature, the weather, and the seasonal values the renderer uses. It is
2//! advanced once per simulation iteration, after the calendar. See docs/ENVIRONMENT.md,
3//! docs/GAME-LOOP-STEP-04.md and docs/GAME-LOOP-STEP-05.md.
4//!
5//! Original: `FUN_140099430` (set-up), `FUN_140099300` (bound to the climate), `FUN_1400999b0`
6//! (the update).
7// Game/Game +0x130
8
9use banished_formats::game::ClimateGenerator;
10
11use crate::noise::PerlinNoise;
12use crate::random::Random;
13
14// DAT_14017895c
15/// Seconds in a game year.
16pub const YEAR_SECONDS: f32 = 3600.0;
17/// `DAT_1401788e4`: weather slices per year.
18const SLICES: f32 = 48.0;
19/// `DAT_140178800`, `DAT_140178894`: temperature Perlin persistence and octaves.
20const NOISE_PERSISTENCE: f32 = 0.25;
21const NOISE_OCTAVES: f32 = 4.0;
22/// `DAT_1401787b4` / `DAT_140178848`: smoothing weights of the annual targets.
23const SMOOTH_NEW: f32 = 0.000_999_987_1;
24const SMOOTH_OLD: f32 = 0.999;
25/// `DAT_1401787e0`: 1/12.
26const TWELFTH: f32 = 0.083_333_336;
27/// `DAT_140178920`: seconds per Perlin unit.
28const NOISE_PERIOD: f32 = 180.0;
29/// `DAT_140178934`: seconds per month (also the sine phase offset).
30const MONTH_SECONDS: f32 = 300.0;
31// DAT_1401788d8
32/// Rain above this temperature, Snow at or below.
33pub const RAIN_SNOW_THRESHOLD: f32 = 34.0;
34// DAT_1401788d0
35/// Freezing point (°F).
36pub const FREEZING: f32 = 32.0;
37/// Game option `DAT_1401cbde0 + 0x16` (weather effects), on by default (`FUN_140042850`).
38const WEATHER_EFFECTS: bool = true;
39
40// +0x50; +0x60
41// The geometry is made by FUN_1400d93e0.
42/// A weather effect slot: its particle geometry and its ambient sound. Slot 0 is rain, slot 1 is
43/// snow (the order of the environment's weather list).
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct WeatherEffect {
46    // geometry +0xc0
47    /// `geometry`: 0..1, also the ambient emitter's volume.
48    pub intensity: f32,
49    // geometry +0xc4
50    /// `geometry`: +1 fading in, −1 fading out.
51    pub direction: f32,
52}
53
54/// The climate fields the controller reads (`ClimateGenerator`).
55///
56/// Original: `FUN_140098470`.
57#[derive(Debug, Clone, PartialEq)]
58pub struct Climate {
59    // +0x34
60    /// Baseline temperature.
61    pub baseline: f32,
62    // +0x38
63    /// Amplitude of the yearly sine.
64    pub seasonal: f32,
65    // +0x3c
66    /// Amplitude of the Perlin noise.
67    pub noise: f32,
68    // +0x40
69    /// Range of the annual random baseline offset.
70    pub baseline_range: f32,
71    // +0x44
72    /// Four floats per month; the first three are cumulative weather-state cutoffs.
73    pub months: [[f32; 4]; 12],
74    // +0x104
75    /// Range of the annual weather-cutoff bias.
76    pub bias_range: f32,
77    // +0x108, +0x10c
78    /// A tornado's odds while it rains (years, odds).
79    ///
80    /// Original: `FUN_1400ae760`.
81    pub tornado: (f32, f32),
82}
83
84impl From<&ClimateGenerator> for Climate {
85    fn from(c: &ClimateGenerator) -> Climate {
86        Climate {
87            baseline: c.settings[0],
88            seasonal: c.settings[1],
89            noise: c.settings[2],
90            baseline_range: c.settings[3],
91            months: c.monthly_thresholds,
92            bias_range: c.trailing_settings[0],
93            tornado: (c.trailing_settings[1], c.trailing_settings[2]),
94        }
95    }
96}
97
98/// The values the renderer receives from the environment each update (the shaders'
99/// `pc_userPosition`).
100///
101/// Original: `FUN_1400988f0`.
102#[derive(Debug, Clone, Copy, Default, PartialEq)]
103pub struct SeasonValues {
104    // +0x1c
105    /// Snow cover, `clamp(32 − temperature, 0, 1)`.
106    pub snow: f32,
107    // +0x20
108    /// Autumn tint.
109    pub autumn: f32,
110    // +0x24
111    /// Leaf loss.
112    pub leaf_loss: f32,
113    // +0x28; geometry +0xc0
114    /// Rain, the strongest active weather effect's intensity (`geometry`).
115    pub rain: f32,
116}
117
118/// The environment: temperature, weather and the renderer's seasonal values. See the [module
119/// documentation](self).
120#[derive(Debug, Clone)]
121pub struct Environment {
122    /// The climate.
123    pub climate: Climate,
124    // +0x10
125    /// 0 precipitation, 1 cloudy, 2 partly sunny, 3 sunny.
126    pub state: i32,
127    // +0x14
128    /// Precipitation level 0..5 (starts at 4).
129    pub level: i32,
130    // +0x18
131    /// °F.
132    pub temperature: f32,
133    /// The renderer's seasonal values.
134    pub season: SeasonValues,
135    /// `+0x2c` / `+0x30`: smoothed and target annual baseline.
136    baseline: f32,
137    baseline_target: f32,
138    /// `+0x34`: temperature noise.
139    noise: PerlinNoise,
140    /// `+0x44` / `+0x48`: smoothed and target weather-cutoff bias.
141    bias: f32,
142    bias_target: f32,
143    /// `+0x4c`: last processed weather slice (−1 before the first update).
144    slice: i32,
145    /// The Rain and Snow effects while they exist.
146    pub effects: [Option<WeatherEffect>; 2],
147    // Weather +0x5c
148    /// `Weather` of each slot's description: seconds to fade in or out. A slot without a loaded
149    /// description never starts.
150    pub fade_times: [Option<f32>; 2],
151    // +0x70
152    /// The time since the last tornado check.
153    pub tornado_timer: i64,
154    // Game +0xd0
155    /// `Game`: bit 7 stops tornadoes (set with disasters off).
156    pub game_flags: u32,
157}
158
159/// `floor(elapsed / 3600 × 48)`, wrapped to 0 past 47.
160fn weather_slice(elapsed: f32) -> i32 {
161    let i = ((elapsed / YEAR_SECONDS) * SLICES) as i32;
162    if i > 0x2f {
163        0
164    } else {
165        i
166    }
167}
168
169impl Environment {
170    // State +0x10, level +0x14, temperature +0x18, renderer values +0x1c, baseline +0x2c/+0x30,
171    // bias +0x44/+0x48, last slice +0x4c; each effect re-created by FUN_140099790 with its +0xc4
172    // direction and +0xc0 intensity.
173    /// The environment as a save stores it: the state, the level, the temperature, the renderer's
174    /// values, the baseline, the bias, the last slice, and each weather effect that exists (made
175    /// again on load with its direction and intensity).
176    ///
177    /// Original: `FUN_140099160`.
178    ///
179    /// Deviation: the temperature noise (`+0x34`) is not saved; after a load the original keeps
180    /// its constructor's clock-seeded field until the next reseed (slice 0 or 24). The port
181    /// keeps its constructor's fixed seed.
182    #[allow(clippy::too_many_arguments)]
183    pub fn restore(
184        &mut self,
185        state: i32,
186        level: i32,
187        temperature: f32,
188        season: SeasonValues,
189        baseline: (f32, f32),
190        bias: (f32, f32),
191        slice: i32,
192        effects: [Option<WeatherEffect>; 2],
193    ) {
194        self.state = state;
195        self.level = level;
196        self.temperature = temperature;
197        self.season = season;
198        (self.baseline, self.baseline_target) = baseline;
199        (self.bias, self.bias_target) = bias;
200        self.slice = slice;
201        self.effects = effects;
202    }
203
204    /// What [`Environment::restore`] takes, for saving: baseline and bias with their targets,
205    /// and the last slice.
206    pub fn saved(&self) -> ((f32, f32), (f32, f32), i32) {
207        (
208            (self.baseline, self.baseline_target),
209            (self.bias, self.bias_target),
210            self.slice,
211        )
212    }
213
214    /// A new environment for a climate: state 0, level 4, and the climate's baseline.
215    ///
216    /// Original: `FUN_140099430`, then `FUN_140099300`.
217    ///
218    /// Deviation: the constructor seeds its first Perlin field from two clock-seeded `Random()`s;
219    /// the first update always reseeds it (slice 0), so a fixed seed is used here.
220    pub fn new(climate: Climate) -> Environment {
221        Environment {
222            state: 0,
223            level: 4,
224            temperature: 0.0,
225            season: SeasonValues::default(),
226            baseline: climate.baseline,
227            baseline_target: climate.baseline,
228            noise: PerlinNoise::new(NOISE_PERSISTENCE, NOISE_OCTAVES, 0, 0),
229            bias: 0.0,
230            bias_target: 0.0,
231            slice: -1,
232            effects: [None; 2],
233            fade_times: [None; 2],
234            tornado_timer: 0,
235            game_flags: 0,
236            climate,
237        }
238    }
239
240    // Game/Game +0xd8; +0x70; FUN_1400ae760 (climate +0x108)
241    /// One simulation iteration. `elapsed` is the calendar's time into the current year in seconds,
242    /// already advanced for this iteration; `dt` is the iteration's step; `rng` is the game's
243    /// shared random stream (`Game/Game`).
244    ///
245    /// Returns whether a tornado forms (step 6): this can happen while it rains above 34 °F with
246    /// tornadoes on. The caller then creates the environment template's entity.
247    ///
248    /// Original: `FUN_1400999b0`, `FUN_1400c8bf0`, `FUN_1400c9d70`.
249    pub fn update(&mut self, elapsed: f32, dt: f32, rng: &mut Random) -> bool {
250        self.step_effects(dt);
251        self.baseline = self.baseline_target * SMOOTH_NEW + self.baseline * SMOOTH_OLD;
252        self.bias = self.bias_target * SMOOTH_NEW + self.bias * SMOOTH_OLD;
253        self.update_temperature(elapsed);
254        self.update_season(elapsed);
255        let slice = weather_slice(elapsed);
256        if slice != self.slice {
257            self.slice = slice;
258            self.next_weather(slice, rng);
259        }
260        self.switch_effects();
261        self.tornado_timer += crate::scheduler::ticks(dt);
262        if RAIN_SNOW_THRESHOLD < self.temperature && self.state == 0 && self.game_flags & 0x80 == 0
263        {
264            let (years, odds) = self.climate.tornado;
265            if odds <= 0.0 {
266                self.tornado_timer = 0;
267                return false;
268            }
269            let period = years * 12.0 * 300.0;
270            let since = crate::scheduler::seconds(self.tornado_timer);
271            self.tornado_timer = 0;
272            return rng.rand_float() < since / (period * odds);
273        }
274        false
275    }
276
277    /// `FUN_1400999b0`'s first loop: each effect geometry's update (`FUN_1400d8330`) moves its
278    /// intensity by `dt / fade × direction`, clamped to 0..1; the emitter's volume follows. At
279    /// intensity 0 (or with weather effects off) the geometry and its emitter are removed.
280    fn step_effects(&mut self, dt: f32) {
281        for (slot, fade) in self.effects.iter_mut().zip(self.fade_times) {
282            let Some(e) = slot else { continue };
283            let v = dt / fade.unwrap_or(1.0) * e.direction + e.intensity;
284            e.intensity = if v > 1.0 {
285                1.0
286            } else if v < 0.0 {
287                0.0
288            } else {
289                v
290            };
291            if e.intensity == 0.0 || !WEATHER_EFFECTS {
292                *slot = None;
293            }
294        }
295    }
296
297    /// `FUN_1400999b0` → `FUN_140099790` for slots 0 and 1: a slot is wanted when weather
298    /// effects are on, the state is 0 and the temperature picks it (Rain above 34°F, Snow at or
299    /// below). An unwanted existing slot fades out and forces the state to 3; a wanted missing
300    /// slot starts at intensity 0 fading in and sets the state to 0.
301    fn switch_effects(&mut self) {
302        let snow = self.temperature <= RAIN_SNOW_THRESHOLD;
303        for i in 0..2 {
304            let on = WEATHER_EFFECTS && self.state == 0 && snow == (i == 1);
305            if !on {
306                if let Some(e) = &mut self.effects[i] {
307                    e.direction = -1.0;
308                    self.state = 3;
309                }
310            } else if self.effects[i].is_none() && self.fade_times[i].is_some() {
311                self.effects[i] = Some(WeatherEffect {
312                    intensity: 0.0,
313                    direction: 1.0,
314                });
315                self.state = 0;
316            }
317        }
318    }
319
320    /// Which weather effect slot is active: only in state 0 with weather enabled; slot 0 Rain above
321    /// 34°, slot 1 Snow at or below.
322    ///
323    /// Original: `FUN_1400999b0`, `FUN_140099790`.
324    pub fn active_effect(&self, weather_enabled: bool) -> Option<usize> {
325        if !weather_enabled || self.state != 0 {
326            return None;
327        }
328        Some(if self.temperature <= RAIN_SNOW_THRESHOLD {
329            1
330        } else {
331            0
332        })
333    }
334
335    /// `FUN_140098ce0`: smoothed baseline + yearly sine + Perlin noise.
336    fn update_temperature(&mut self, elapsed: f32) {
337        let noise = self.noise.generate(elapsed / NOISE_PERIOD, 0.0);
338        // `FUN_140137160`: sine of `((t − 300) / 3600) × 360 × π/180`.
339        let phase = ((elapsed - MONTH_SECONDS) / YEAR_SECONDS) * 360.0 * 0.017_453_292;
340        let sine = crate::crt::sinf(phase);
341        let t = sine * self.climate.seasonal + self.baseline;
342        self.temperature = noise * self.climate.noise + t;
343    }
344
345    /// `FUN_1400988f0`: snow cover, autumn tint, leaf loss and rain for the shaders.
346    fn update_season(&mut self, elapsed: f32) {
347        let clamp = |v: f32| v.clamp(0.0, 1.0);
348        let snow = clamp((self.temperature - FREEZING) * -1.0);
349        let month = elapsed / MONTH_SECONDS;
350        let autumn = if month >= 6.0 {
351            clamp((month - 6.0) / 1.5)
352        } else {
353            0.0
354        };
355        let leaf_loss = if month < 0.5 {
356            1.0 - clamp(month / 0.5)
357        } else if month <= 7.5 {
358            0.0
359        } else {
360            clamp((month - 7.5) * 2.0)
361        };
362        let rain = self.effects.iter().flatten().fold(0.0f32, |m, e| {
363            if m < e.intensity {
364                e.intensity
365            } else {
366                m
367            }
368        });
369        self.season = SeasonValues {
370            snow,
371            autumn,
372            leaf_loss,
373            rain,
374        };
375    }
376
377    /// `FUN_140098ac0`: at slices 0 and 24 reseed the noise and pick new annual targets, then
378    /// draw the weather state from the month's biased cutoffs.
379    fn next_weather(&mut self, slice: i32, rng: &mut Random) {
380        if slice == 0 || slice == 24 {
381            let first = rng.rand();
382            let second = rng.rand();
383            self.noise = PerlinNoise::new(
384                NOISE_PERSISTENCE,
385                NOISE_OCTAVES,
386                second as i32,
387                first as i32,
388            );
389            let (mut bias, mut baseline) = (0.0f32, 0.0f32);
390            for _ in 0..12 {
391                bias += rng.rand_float_tol() * TWELFTH;
392                baseline += rng.rand_float_tol() * TWELFTH;
393            }
394            self.baseline_target = baseline * self.climate.baseline_range + self.climate.baseline;
395            self.bias_target = bias * self.climate.bias_range;
396        }
397        let sample = rng.rand_float();
398        let month = &self.climate.months[(slice / 4) as usize];
399        self.state = 0;
400        while !(sample < self.bias + month[self.state as usize]) {
401            self.state += 1;
402            if self.state >= 3 {
403                break;
404            }
405        }
406        match self.state {
407            0 => self.level += 1,
408            3 => self.level -= 1,
409            _ => {}
410        }
411        self.level = self.level.clamp(0, 5);
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    fn fair() -> Climate {
420        Climate {
421            baseline: 50.0,
422            seasonal: 30.0,
423            noise: 5.0,
424            baseline_range: 40.0,
425            months: [[0.4, 0.5, 0.7, 1.0]; 12],
426            bias_range: 0.4,
427            tornado: (0.0, 0.0),
428        }
429    }
430
431    #[test]
432    fn slices_and_reseeding() {
433        assert_eq!(weather_slice(0.0), 0);
434        assert_eq!(weather_slice(75.0), 1);
435        assert_eq!(weather_slice(3599.0), 47);
436        let mut env = Environment::new(fair());
437        let mut rng = Random::new(290);
438        env.update(0.0, 0.0, &mut rng);
439        assert_eq!(env.slice, 0);
440        // Two Rand + 24 RandFloatTol + one RandFloat consumed.
441        let mut expect = Random::new(290);
442        for _ in 0..2 {
443            expect.rand();
444        }
445        for _ in 0..24 {
446            expect.rand_float_tol();
447        }
448        expect.rand_float();
449        assert_eq!(rng.rand(), expect.rand());
450        assert!((0..=3).contains(&env.state));
451        assert!((0..=5).contains(&env.level));
452    }
453
454    #[test]
455    fn temperature_follows_the_season() {
456        let mut env = Environment::new(Climate {
457            noise: 0.0,
458            ..fair()
459        });
460        let mut rng = Random::new(1);
461        // Month 0 starts at sin(−30°): baseline − 15.
462        env.update(0.0, 0.0, &mut rng);
463        assert!((env.temperature - 35.0).abs() < 0.01, "{}", env.temperature);
464        assert_eq!(env.season.snow, 0.0);
465        // Peak at month 4 (sin 90°).
466        env.update(1200.0, 0.0, &mut rng);
467        assert!(env.temperature > env.baseline + 29.0);
468    }
469
470    #[test]
471    fn rain_fades_in_and_out() {
472        let mut env = Environment::new(fair());
473        env.fade_times = [Some(20.0), Some(20.0)];
474        env.temperature = 50.0;
475        env.state = 0;
476        env.switch_effects();
477        assert_eq!(env.effects[0].unwrap().intensity, 0.0);
478        assert!(env.effects[1].is_none());
479        // 10 s of a 20 s fade.
480        for _ in 0..100 {
481            env.step_effects(0.1);
482        }
483        assert!((env.effects[0].unwrap().intensity - 0.5).abs() < 1e-3);
484        // Too cold for rain: the rain fades out, the state becomes 3 and snow does not start.
485        env.temperature = 30.0;
486        env.switch_effects();
487        assert_eq!(env.state, 3);
488        assert_eq!(env.effects[0].unwrap().direction, -1.0);
489        assert!(env.effects[1].is_none());
490        for _ in 0..101 {
491            env.step_effects(0.1);
492        }
493        assert!(env.effects[0].is_none());
494    }
495
496    #[test]
497    fn leaves_and_autumn() {
498        let mut env = Environment::new(fair());
499        env.update_season(0.0);
500        assert_eq!(env.season.leaf_loss, 1.0);
501        env.update_season(300.0 * 6.75);
502        assert!((env.season.autumn - 0.5).abs() < 1e-6);
503        env.update_season(300.0 * 8.0);
504        assert_eq!(env.season.leaf_loss, 1.0);
505    }
506}