1use crate::random::Random;
17use crate::scheduler::seconds;
18use crate::terrain::HeightGrid;
19use crate::town::Sim;
20use crate::work::jobs::Jobs;
21
22#[derive(Debug, Clone, Copy, Default, PartialEq)]
26pub struct TornadoTemplate {
27 pub speed: f32,
30 pub min_distance: i32,
33 pub interval: i64,
36 pub reach: i32,
39 pub throw: i32,
42 pub fade: i64,
46}
47
48#[derive(Debug, Clone, PartialEq)]
52pub struct Tornado {
53 pub target: [f32; 3],
56 pub path: Vec<[f32; 3]>,
58 pub index: i32,
60 pub timer: i64,
63 pub position: [f32; 3],
66 pub emitting: bool,
70 pub slot: Option<(f32, f32)>,
76}
77
78impl Tornado {
79 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
102const POINTS: usize = 129;
104
105fn normalize(v: [f32; 3]) -> ([f32; 3], f32) {
107 crate::math::normalize3_len(v)
108}
109
110pub 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 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 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 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 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 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 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 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 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 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 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 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}