Skip to main content

banished_game/disasters/
tornado.rs

1//! Tornadoes: the environment forms one while it rains
2//! (`Environment::update`); it crosses the map along a jagged line, flattening crops and trees,
3//! wrecking buildings and killing animals and citizens near it. See docs/TORNADO.md.
4//!
5//! - Setup: two random cells at least apart (at most a hundred tries), a 129-point line between
6//!   them bent by midpoint displacement, the event "a tornado has touched down" (type 30, severity
7//!   3).
8//! - Update: when its particles are gone the entity goes; else it follows the line (within 3
9//!   cells of its point, the next point is 10 further), wrecks what is near or, at the end, stops
10//!   forming particles, and moves a second toward its point on the ground.
11//!
12//! Original: `TornadoComponent`: `FUN_140058f00`, `FUN_1400592e0`, `FUN_140058630`,
13//! `FUN_1400589a0`; following the line, `FUN_1400ac6b0`.
14// vtable 0x140160ba0; slot +0x20; +0x24; slot +0x40; +0x20
15
16use crate::random::Random;
17use crate::scheduler::seconds;
18use crate::terrain::HeightGrid;
19use crate::town::Sim;
20use crate::work::jobs::Jobs;
21
22/// TornadoDescription.
23///
24/// Original: `FUN_1400596f0`.
25#[derive(Debug, Clone, Copy, Default, PartialEq)]
26pub struct TornadoTemplate {
27    // +0x20
28    /// Cells a second.
29    pub speed: f32,
30    // +0x24
31    /// The ends' least distance in cells.
32    pub min_distance: i32,
33    // +0x28
34    /// The time between wrecking passes.
35    pub interval: i64,
36    // +0x30
37    /// The reach of a pass (cells, and the radius about the thrown point).
38    pub reach: i32,
39    // +0x34
40    /// How far from it a pass throws its point.
41    pub throw: i32,
42    // +0x20
43    /// The fade of particle slot 0: once the tornado stops forming particles, the slot's alpha
44    /// falls to 0 over this time and the particle system goes.
45    pub fade: i64,
46}
47
48/// The component's state.
49///
50/// Original: `FUN_1400594e0`.
51#[derive(Debug, Clone, PartialEq)]
52pub struct Tornado {
53    // +0x30; +0x40; +0x50
54    /// The point it heads for.
55    pub target: [f32; 3],
56    /// The line it follows.
57    pub path: Vec<[f32; 3]>,
58    /// The index of the point it heads for.
59    pub index: i32,
60    // +0x28
61    /// Until the next wrecking pass.
62    pub timer: i64,
63    // +0x58
64    /// Where it is.
65    pub position: [f32; 3],
66    /// Whether its particle slot is on.
67    ///
68    /// Original: `FUN_140054080(0)` fades it out.
69    pub emitting: bool,
70    // slot +0x10; +0x7c
71    /// The particle slot's alpha (system) and its rate per second: `1 / fade` from the start, `−1 /
72    /// fade` once it stops; `None` once the slot is gone.
73    ///
74    /// Original: `FUN_140053fc0`.
75    pub slot: Option<(f32, f32)>,
76}
77
78impl Tornado {
79    /// The particle component's update for the slot, which runs before the tornado's (the template
80    /// lists `particle` first): the alpha moves by `AsSeconds(dt) × rate`; above 1 it stays at 1
81    /// and stops, at 0 or below the slot goes.
82    ///
83    /// Original: `FUN_140053490`.
84    pub fn step_slot(&mut self, dt: i64) {
85        let Some((alpha, rate)) = &mut self.slot else {
86            return;
87        };
88        if *rate != 0.0 {
89            *alpha += seconds(dt) * *rate;
90            if 1.0 < *alpha {
91                *alpha = 1.0;
92                *rate = 0.0;
93                return;
94            }
95        }
96        if *alpha <= 0.0 {
97            self.slot = None;
98        }
99    }
100}
101
102/// `PATH`: the line's points (`0x81`).
103const POINTS: usize = 129;
104
105/// `Math::Vector3::Normalize` with the length.
106fn normalize(v: [f32; 3]) -> ([f32; 3], f32) {
107    crate::math::normalize3_len(v)
108}
109
110/// Bend a line by midpoint displacement: the point midway between `a` and `b` moves off the line,
111/// to its left, by a random amount up to a quarter of their distance; then each half is bent the
112/// same way.
113///
114/// Original: `FUN_1400592e0(path, a, b)`.
115pub fn displace(path: &mut [[f32; 3]], a: usize, b: usize, rng: &mut Random) {
116    let (p, q) = (path[a], path[b]);
117    let (d, length) = normalize([q[0] - p[0], q[1] - p[1], q[2] - p[2]]);
118    let mid = [
119        (p[0] + q[0]) * 0.5,
120        (p[1] + q[1]) * 0.5,
121        (p[2] + q[2]) * 0.5,
122    ];
123    let (side, _) = normalize([-d[1], d[0], 0.0]);
124    let m = (a + b) >> 1;
125    let r = rng.rand_float();
126    let s = length * 0.25;
127    path[m] = [
128        side[0] * s * r + mid[0],
129        side[1] * s * r + mid[1],
130        side[2] * s * r + mid[2],
131    ];
132    if 1 < m - a {
133        displace(path, a, m, rng);
134    }
135    if 1 < b - m {
136        displace(path, m, b, rng);
137    }
138}
139
140impl Tornado {
141    /// A new tornado on a map of `size` cells: its line across the map, and its event.
142    ///
143    /// Original: `FUN_140058f00`.
144    pub fn new(
145        t: &TornadoTemplate,
146        size: usize,
147        terrain: &HeightGrid,
148        floors: &crate::world::floor::Floors,
149        rng: &mut Random,
150    ) -> Tornado {
151        let n = size as u32;
152        let mut tries = 0;
153        let (mut x0, mut y0, mut x1, mut y1);
154        loop {
155            x0 = rng.rand_below(n) as i32;
156            y0 = rng.rand_below(n) as i32;
157            x1 = rng.rand_below(n) as i32;
158            y1 = rng.rand_below(n) as i32;
159            if 99 < tries {
160                break;
161            }
162            tries += 1;
163            let (dx, dy) = (x1 - x0, y1 - y0);
164            if t.min_distance * t.min_distance <= dx * dx + dy * dy {
165                break;
166            }
167        }
168        // `FUN_1400a48b0(.., 0)` at the cells' centres: a floor counts.
169        let ground =
170            |x: i32, y: i32| crate::town::floor_ground(floors, size, terrain, y * size as i32 + x);
171        let start = ground(x0, y0);
172        let end = ground(x1, y1);
173        let mut path = vec![[0.0; 3]; POINTS];
174        path[0] = [start[0], start[1], 0.0];
175        path[POINTS - 1] = [end[0], end[1], 0.0];
176        displace(&mut path, 0, POINTS - 1, rng);
177        Tornado {
178            target: path[0],
179            path,
180            index: 0,
181            timer: 0,
182            position: start,
183            emitting: true,
184            slot: Some((0.0, 1.0 / seconds(t.fade))),
185        }
186    }
187
188    /// Within 3 of its point, the next point at least 10 on; false past the end.
189    ///
190    /// Original: `FUN_1400ac6b0(state, position, 10, 3)`, `FUN_1400ac590`.
191    pub fn follow(&mut self) -> bool {
192        let p = [self.position[0], self.position[1], 0.0];
193        let (dx, dy) = (self.target[0] - p[0], self.target[1] - p[1]);
194        if (dx * dx + dy * dy).sqrt() < 3.0 {
195            let mut i = self.index;
196            loop {
197                i += 1;
198                if self.path.len() as i32 <= i {
199                    return false;
200                }
201                let q = self.path[i as usize];
202                let (dx, dy, dz) = (p[0] - q[0], p[1] - q[1], p[2]);
203                if 100.0 <= dx * dx + dy * dy + dz * dz {
204                    break;
205                }
206            }
207            self.index = i;
208            let q = self.path[i as usize];
209            self.target = [q[0], q[1], 0.0];
210        }
211        true
212    }
213
214    // +0x34
215    /// The point a wrecking pass throws: `RandFloatTol × 180°` about it, up to away (`RandFloat`).
216    ///
217    /// Original: `FUN_1400589a0`.
218    pub fn throw(&self, t: &TornadoTemplate, rng: &mut Random) -> [f32; 3] {
219        let angle = rng.rand_float_tol() * 180.0 * 0.017_453_292;
220        let r = rng.rand_float();
221        let reach = t.throw as f32;
222        let (sin, cos) = (crate::crt::sinf(angle), crate::crt::cosf(angle));
223        let v = [sin * reach, cos * reach, reach * 0.0];
224        [
225            v[0] * r + self.position[0],
226            v[1] * r + self.position[1],
227            v[2] * r + self.position[2],
228        ]
229    }
230}
231
232impl Sim<'_, '_> {
233    /// Every tornado's update for `dt` ticks.
234    ///
235    /// Original: `FUN_140058630`.
236    pub fn update_tornadoes(&mut self, jobs: &mut Jobs, dt: i64) {
237        let ids: Vec<usize> = self.tornadoes.keys().copied().collect();
238        for e in ids {
239            let Some(t) = self.templates[self.placed[e].template].tornado else {
240                continue;
241            };
242            let tornado = self.tornadoes.get_mut(&e).unwrap();
243            tornado.step_slot(dt);
244            // Its particle system is gone (the slot faded out): the entity goes
245            // (`FUN_1400c8370`).
246            if tornado.slot.is_none() {
247                self.tornadoes.remove(&e);
248                self.placed[e].alive = false;
249                *self.models_dirty = true;
250                continue;
251            }
252            let going = tornado.follow();
253            let (d, _) = normalize([
254                tornado.target[0] - tornado.position[0],
255                tornado.target[1] - tornado.position[1],
256                tornado.target[2] - tornado.position[2],
257            ]);
258            if !going {
259                // `FUN_140054080(0)`: the slot fades out at `−(1 / AsSeconds(fade))`.
260                tornado.emitting = false;
261                if let Some((_, rate)) = &mut tornado.slot {
262                    *rate = -(1.0 / seconds(t.fade));
263                }
264            } else {
265                self.wreck(jobs, e, &t, dt);
266            }
267            let tornado = self.tornadoes.get_mut(&e).unwrap();
268            let step = seconds(dt) * t.speed;
269            let p = [
270                d[0] * step + tornado.position[0],
271                d[1] * step + tornado.position[1],
272                d[2] * step + tornado.position[2],
273            ];
274            // `FUN_1400a48b0(…, skip floors)`.
275            let mut normal = [0.0; 3];
276            tornado.position = [p[0], p[1], self.terrain.ground(p[0], p[1], &mut normal)];
277            self.placed[e].position = tornado.position;
278        }
279    }
280
281    /// `FUN_1400589a0`: every `+0x28` a pass: a point thrown about the tornado, the natural
282    /// resource on its cell, every entity within `+0x30` of it (`FUN_140097e60`), then the
283    /// objects of every cell within `+0x30` of the tornado (`FUN_140058d20` each).
284    fn wreck(&mut self, jobs: &mut Jobs, e: usize, t: &TornadoTemplate, dt: i64) {
285        let tornado = self.tornadoes.get_mut(&e).unwrap();
286        tornado.timer -= dt;
287        if 0 < tornado.timer {
288            return;
289        }
290        tornado.timer += t.interval;
291        let point = tornado.throw(t, self.rng);
292        let position = tornado.position;
293        let n = self.size as i32;
294        let half = (n >> 1) as f32;
295        let cell = |p: [f32; 3]| {
296            let x = ((p[0] + half) as i32).clamp(0, n - 1);
297            let y = ((p[1] + half) as i32).clamp(0, n - 1);
298            (x, y)
299        };
300        let (x, y) = cell(point);
301        if let Some(o) = self.cells.objects[(y * n + x) as usize] {
302            if self.templates[self.placed[o].template].natural.is_some() {
303                self.hit(jobs, Hit::Entity(o));
304            }
305        }
306        // `FUN_140097e60` (type 0): the characters within the reach of the point, in the
307        // characters' tree's order. Only characters: the buildings and resources are reached
308        // by the cells about the tornado below. The tree's z bounds leave out everyone while
309        // the point stands more than the reach + 1 from height 0. A nomad or a boat has none
310        // of what `FUN_140058d20` acts on.
311        let hits: Vec<Hit> = self
312            .wildlife
313            .tree
314            .within(point, t.reach as f32, |_| true)
315            .into_iter()
316            .filter_map(|who| match who {
317                crate::behaviour::character_tree::Who::Citizen(c) => Some(Hit::Citizen(c)),
318                crate::behaviour::character_tree::Who::Animal(a) => Some(Hit::Entity(a)),
319                _ => None,
320            })
321            .collect();
322        for h in hits {
323            self.hit(jobs, h);
324        }
325        let (cx, cy) = cell(position);
326        for y in cy - t.reach..=cy + t.reach {
327            for x in cx - t.reach..=cx + t.reach {
328                if x < 0 || y < 0 || n <= x || n <= y {
329                    continue;
330                }
331                if let Some(o) = self.cells.objects[(y * n + x) as usize] {
332                    self.hit(jobs, Hit::Entity(o));
333                }
334            }
335        }
336    }
337
338    /// `FUN_140058d20`: a growing natural resource (a crop, a tree) not already falling loses
339    /// its growth and is removed; a building that burns (Build and Fire) and is not damaged
340    /// yet is wrecked to rubble; an animal goes (`FUN_1400c9ea0`); a citizen dies (code 5).
341    fn hit(&mut self, jobs: &mut Jobs, h: Hit) {
342        let e = match h {
343            Hit::Citizen(c) => {
344                if !self.tornado_kills.contains(&c) {
345                    self.tornado_kills.push(c);
346                }
347                return;
348            }
349            Hit::Entity(e) => e,
350        };
351        if !self.placed[e].alive {
352            return;
353        }
354        let template = &self.templates[self.placed[e].template];
355        if template.natural.is_some() && template.growth.is_some() {
356            if self.placed[e].animation.is_none() && !self.placed[e].removal_requested {
357                self.placed[e].growth = 0.0;
358                self.placed[e].removal_requested = true;
359                self.animate_resource(e, 2);
360                *self.models_dirty = true;
361            }
362            return;
363        }
364        let building = self
365            .sites
366            .get(&e)
367            .is_some_and(|s| s.fire.is_some() && !s.destroyed)
368            && self.has_build(e);
369        if building {
370            if !self.is_damaged(e) && self.damage(jobs, e, 0) {
371                self.to_rubble(jobs, e);
372            }
373            return;
374        }
375        if self.wildlife.animals.contains_key(&e) {
376            self.wildlife.kill(e);
377        }
378    }
379}
380
381#[derive(Debug, Clone, Copy)]
382enum Hit {
383    Entity(usize),
384    Citizen(usize),
385}