1use banished_formats::game::ClimateGenerator;
10
11use crate::noise::PerlinNoise;
12use crate::random::Random;
13
14pub const YEAR_SECONDS: f32 = 3600.0;
17const SLICES: f32 = 48.0;
19const NOISE_PERSISTENCE: f32 = 0.25;
21const NOISE_OCTAVES: f32 = 4.0;
22const SMOOTH_NEW: f32 = 0.000_999_987_1;
24const SMOOTH_OLD: f32 = 0.999;
25const TWELFTH: f32 = 0.083_333_336;
27const NOISE_PERIOD: f32 = 180.0;
29const MONTH_SECONDS: f32 = 300.0;
31pub const RAIN_SNOW_THRESHOLD: f32 = 34.0;
34pub const FREEZING: f32 = 32.0;
37const WEATHER_EFFECTS: bool = true;
39
40#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct WeatherEffect {
46 pub intensity: f32,
49 pub direction: f32,
52}
53
54#[derive(Debug, Clone, PartialEq)]
58pub struct Climate {
59 pub baseline: f32,
62 pub seasonal: f32,
65 pub noise: f32,
68 pub baseline_range: f32,
71 pub months: [[f32; 4]; 12],
74 pub bias_range: f32,
77 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#[derive(Debug, Clone, Copy, Default, PartialEq)]
103pub struct SeasonValues {
104 pub snow: f32,
107 pub autumn: f32,
110 pub leaf_loss: f32,
113 pub rain: f32,
116}
117
118#[derive(Debug, Clone)]
121pub struct Environment {
122 pub climate: Climate,
124 pub state: i32,
127 pub level: i32,
130 pub temperature: f32,
133 pub season: SeasonValues,
135 baseline: f32,
137 baseline_target: f32,
138 noise: PerlinNoise,
140 bias: f32,
142 bias_target: f32,
143 slice: i32,
145 pub effects: [Option<WeatherEffect>; 2],
147 pub fade_times: [Option<f32>; 2],
151 pub tornado_timer: i64,
154 pub game_flags: u32,
157}
158
159fn 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 #[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 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 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 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 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 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 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 fn update_temperature(&mut self, elapsed: f32) {
337 let noise = self.noise.generate(elapsed / NOISE_PERIOD, 0.0);
338 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 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 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 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 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 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 for _ in 0..100 {
481 env.step_effects(0.1);
482 }
483 assert!((env.effects[0].unwrap().intensity - 0.5).abs() < 1e-3);
484 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}