Skip to main content

banished_game/disasters/
fire.rs

1//! Fire: FireComponent, its work FireWork, WellComponent and the town's water sources (`Game`:
2//! shore points, wells, per 8 × 8 block). See docs/FIRE.md.
3//!
4//! A building catches fire now and then (or from a burning neighbour), is damaged for half the burn
5//! (DestroyComponent), and marks the cells around it as an emergency: citizens there drop what they
6//! do and carry buckets from the nearest water to it. Enough buckets, or the burn running out, put
7//! it out; it is left as rubble.
8//!
9//! Original: `FUN_1400055e0`, `FUN_14005da70`.
10// vtable 0x14015f480; vtable 0x1401676c0; vtable 0x140160fb0; Game +0xf0; +0x130; +0xe0; +0x140
11
12use banished_formats::game::FireDescription;
13
14use crate::behaviour::character::{work_facing, Action, Component, Message, MoveTo};
15use crate::nature::seeding::Cells;
16use crate::scheduler::{seconds, ticks};
17use crate::town::Sim;
18use crate::work::jobs::{Jobs, Work, WorkKind};
19
20/// `DAT_140178934`.
21const PERIOD: f32 = 300.0;
22// +0x22; Map +0x9c
23/// The map layer counting fires over each cell (total `Map`).
24pub const LAYER: usize = 6;
25/// FireWork's registration mask (category 14, emergencies).
26const MASK: u32 = 0x4000;
27/// Event type 31: a building caught fire.
28const CAUGHT: i32 = 0x1f;
29// Game +0xd0
30/// `Game` bit 6: fires are off (no disasters).
31pub const NO_FIRES: u32 = 0x40;
32/// `Map::Reachable` mask of the water search.
33const WATER_MASK: u32 = 0xe07;
34/// `DAT_140178824`, `DAT_140178888`.
35const THROW: f32 = 0.75;
36const RUN: f32 = 3.5;
37
38// +0x28; +0x2c; +0x30; +0x38; +0x40
39/// FireComponent: buckets thrown, buckets at the last spread, burn left (burning while above zero),
40/// towards the next spread, towards the next ignition check.
41#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct Fire {
43    /// The buckets thrown.
44    pub buckets: i32,
45    /// The buckets at the last spread.
46    pub buckets_seen: i32,
47    /// The burn left; burning while above zero.
48    pub burn: i64,
49    /// The time towards the next spread.
50    pub spread: i64,
51    /// The time towards the next ignition check.
52    pub timer: i64,
53}
54
55/// The town's water sources.
56#[derive(Debug, Clone, Default)]
57pub struct Water {
58    // +0x130
59    /// (shore cell, water cell) pairs.
60    pub points: Vec<(i32, i32)>,
61    // +0xe0
62    /// WellComponents (a removed well leaves its slot empty).
63    pub wells: Vec<Option<usize>>,
64    // +0x140
65    /// Per 8 × 8 block, the point indices (wells as `-1 - i`).
66    pub blocks: Vec<Vec<i32>>,
67    /// Cells per map side.
68    pub size: i32,
69}
70
71impl Water {
72    /// Every ground cell (type 2) next to river or lake water (types 7, 8) is a point, with the
73    /// first such neighbour by rows.
74    ///
75    /// Original: `FUN_14008e050`, `FUN_1400a43f0` (scan).
76    pub fn new(cells: &Cells) -> Water {
77        let n = cells.size as i32;
78        let b = (n >> 3).max(0) as usize;
79        let mut water = Water {
80            blocks: vec![Vec::new(); b * b],
81            size: n,
82            ..Default::default()
83        };
84        for y in 0..n {
85            for x in 0..n {
86                let cell = y * n + x;
87                if cells.types[cell as usize].0 != 2 {
88                    continue;
89                }
90                'found: for dy in -1..=1 {
91                    for dx in -1..=1 {
92                        let (wx, wy) = (x + dx, y + dy);
93                        if wx < 0 || wy < 0 || n <= wx || n <= wy {
94                            continue;
95                        }
96                        let w = wy * n + wx;
97                        if (7..=8).contains(&cells.types[w as usize].0) {
98                            let i = water.points.len() as i32;
99                            water.points.push((cell, w));
100                            water.add_entry(x, y, i);
101                            break 'found;
102                        }
103                    }
104                }
105            }
106        }
107        water
108    }
109
110    fn block(&self, x: i32, y: i32) -> Option<usize> {
111        let b = self.size >> 3;
112        let (bx, by) = (x >> 3, y >> 3);
113        (0 <= bx && bx < b && 0 <= by && by < b).then_some((b * by + bx) as usize)
114    }
115
116    /// Add a value to the map layer at a cell.
117    ///
118    /// Original: `FUN_14008df50`.
119    pub fn add_entry(&mut self, x: i32, y: i32, v: i32) {
120        if let Some(k) = self.block(x, y) {
121            self.blocks[k].push(v);
122        }
123    }
124
125    /// `FUN_14008deb0`.
126    fn remove_entry(&mut self, x: i32, y: i32, v: i32) {
127        if let Some(k) = self.block(x, y) {
128            self.blocks[k].retain(|&e| e != v);
129        }
130    }
131
132    /// The first empty slot, else a new one, filed under the well's map cell.
133    ///
134    /// Original: `FUN_140097180`.
135    pub fn add_well(&mut self, e: usize, cell: (i32, i32)) {
136        let i = match self.wells.iter().position(Option::is_none) {
137            Some(i) => {
138                self.wells[i] = Some(e);
139                i
140            }
141            None => {
142                self.wells.push(Some(e));
143                self.wells.len() - 1
144            }
145        };
146        self.add_entry(cell.0, cell.1, -1 - i as i32);
147    }
148
149    /// Remove a well from the fire-fighting layer.
150    ///
151    /// Original: `FUN_140097070`.
152    pub fn remove_well(&mut self, e: usize, cell: (i32, i32)) {
153        if let Some(i) = self.wells.iter().position(|&w| w == Some(e)) {
154            self.remove_entry(cell.0, cell.1, -1 - i as i32);
155            self.wells[i] = None;
156        }
157    }
158}
159
160/// A fire's layer over the home's or the workplace's map-rectangle centre, or the citizen's own
161/// cell.
162///
163/// Original: `Citizen::HasEmergency`, `FUN_14011b0b0`.
164pub fn has_emergency(
165    cells: &Cells,
166    placed: &[crate::nature::seeding::Placed],
167    home: Option<usize>,
168    workplace: Option<usize>,
169    position: [f32; 3],
170) -> bool {
171    if cells.layer_totals[LAYER] < 1 {
172        return false;
173    }
174    let n = cells.size as i32;
175    for e in [home, workplace].into_iter().flatten() {
176        let p = &placed[e];
177        let cell = (p.cell.1 + (p.footprint.1 >> 1)) * n + p.cell.0 + (p.footprint.0 >> 1);
178        if cells.layers[cell as usize][LAYER] != 0 {
179            return true;
180        }
181    }
182    let half = (n >> 1) as f32;
183    let x = ((position[0] + half) as i32).clamp(0, n - 1);
184    let y = ((position[1] + half) as i32).clamp(0, n - 1);
185    cells.layers[(y * n + x) as usize][LAYER] != 0
186}
187
188impl Sim<'_, '_> {
189    fn fire_desc(&self, e: usize) -> Option<FireDescription> {
190        self.site_templates[self.sites.get(&e)?.template].fire
191    }
192
193    fn fire(&mut self, e: usize) -> Option<&mut Fire> {
194        self.sites.get_mut(&e)?.fire.as_mut()
195    }
196
197    /// FireComponent's scheduled update of every building that has one: not burning, the ignition
198    /// odds (unless fires are off); burning, the spread and the burn running out.
199    ///
200    /// Original: `FUN_140049910`.
201    pub fn update_fires(&mut self, jobs: &mut Jobs, visits: &[(usize, i64)]) {
202        for &(e, dt) in visits {
203            if !self
204                .sites
205                .get(&e)
206                .is_some_and(|s| s.fire.is_some() && !s.destroyed)
207            {
208                continue;
209            }
210            let Some(d) = self.fire_desc(e) else { continue };
211            let off = self.game_flags & NO_FIRES != 0;
212            let Some(f) = self.fire(e) else { continue };
213            if f.burn == 0 {
214                if off {
215                    continue;
216                }
217                f.timer += dt;
218                if self.odds(e, d.years, d.odds) {
219                    self.ignite(jobs, e);
220                }
221                continue;
222            }
223            f.burn -= dt;
224            f.spread -= dt;
225            if 0 < f.burn && f.spread < 1 {
226                self.spread_fire(jobs, e, &d);
227            }
228            if self.fire(e).is_some_and(|f| f.burn < 1) {
229                self.put_out(jobs, e);
230            }
231        }
232    }
233
234    /// `FUN_1400ae760(timer, [years, odds], rng)`: one in `years × 12 × 300 s × odds` per
235    /// second since the last check; the time restarts.
236    fn odds(&mut self, e: usize, years: f32, odds: f32) -> bool {
237        let f = self.fire(e).unwrap();
238        let elapsed = seconds(f.timer);
239        f.timer = 0;
240        if odds <= 0.0 {
241            return false;
242        }
243        let span = years * 12.0 * PERIOD;
244        self.rng.rand_float() < elapsed / (span * odds)
245    }
246
247    // +0x17
248    /// A building neither damaged nor being torn down is damaged for half the burn; it burns, gets
249    /// its FireWork (prioritised), marks the emergency around it and, as the first fire, posts
250    /// event 31. The particles and the fire sound follow `burn` in the app (docs/PARTICLES.md,
251    /// `Ambience::drive_entities`). (AutoSlow, settings, comes from the event: `EventLog::add`.)
252    ///
253    /// Original: `FireComponent::Ignite`, `FUN_140049f70`.
254    pub fn ignite(&mut self, jobs: &mut Jobs, e: usize) {
255        if self.is_damaged(e) || self.is_demolishing(e) {
256            return;
257        }
258        let Some(d) = self.fire_desc(e) else { return };
259        let total = ticks(d.burn_months * PERIOD + d.burn);
260        if !self.damage(jobs, e, ticks(seconds(total) * 0.5)) {
261            return;
262        }
263        if let Some(f) = self.fire(e) {
264            f.burn = total;
265            f.spread = ticks(d.interval);
266            f.buckets = 0;
267            f.buckets_seen = 0;
268        }
269        let work = Work::new(
270            e,
271            self.rect_cell(e),
272            self.professions.laborer,
273            WorkKind::Fire(crate::production::herbalist::HerbalistWork::default()),
274        );
275        self.set_work(jobs, e, work, MASK, false);
276        let id = self.sites[&e].work_id;
277        jobs.prioritize(id);
278        if self.cells.layer_totals[LAYER] < 1 {
279            self.site_events.push((CAUGHT, 3, e));
280        }
281        self.mark_emergency(e, &d, 1);
282    }
283
284    /// `FUN_14004a320`: the fire layer over the map rectangle grown by the radius
285    /// (`FUN_1400a4f50`).
286    fn mark_emergency(&mut self, e: usize, d: &FireDescription, delta: i32) {
287        let (x, y) = self.placed[e].cell;
288        let (w, h) = self.placed[e].footprint;
289        let r = d.radius;
290        let n = self.size as i32;
291        for cy in y - r..=y + h - 1 + r {
292            for cx in x - r..=x + w - 1 + r {
293                if 0 <= cx && 0 <= cy && cx < n && cy < n {
294                    let v = &mut self.cells.layers[(cy * n + cx) as usize][LAYER];
295                    *v = (*v as i32 + delta) as u8;
296                }
297            }
298        }
299        self.cells.layer_totals[LAYER] += delta as i16;
300    }
301
302    /// It is rubble now; its work and emergency go; the particles fade and the fire sound stops
303    /// with `burn` at 0 (in the app).
304    ///
305    /// Original: `FireComponent::PutOut`, `FUN_140049e20`.
306    pub fn put_out(&mut self, jobs: &mut Jobs, e: usize) {
307        let Some(d) = self.fire_desc(e) else { return };
308        if let Some(f) = self.fire(e) {
309            f.burn = 0;
310            f.spread = 0;
311        }
312        self.to_rubble(jobs, e);
313        self.remove_work(jobs, e);
314        self.mark_emergency(e, &d, -1);
315    }
316
317    /// FireComponent's message slot: message 0, a bucket of water.
318    ///
319    /// Original: `FUN_1400499f0`.
320    pub fn fire_message(&mut self, jobs: &mut Jobs, e: usize, msg: i32) {
321        if msg != 0 {
322            return;
323        }
324        let Some(d) = self.fire_desc(e) else { return };
325        let Some(f) = self.fire(e) else { return };
326        f.buckets += 1;
327        f.burn -= ticks(d.interval * PERIOD / d.buckets as f32);
328        if f.burn < 1 || d.buckets <= f.buckets {
329            self.put_out(jobs, e);
330        }
331    }
332
333    /// `FUN_140049a90`: each spread interval due, a random cell within `min(w, h)` of the
334    /// rectangle; a building there with a FireComponent catches with its odds (halved in
335    /// rain, and halved again when more than a fifth of its buckets were thrown here since the
336    /// last spread).
337    fn spread_fire(&mut self, jobs: &mut Jobs, e: usize, d: &FireDescription) {
338        let n = self.size as i32;
339        let (x, y) = self.placed[e].cell;
340        let (w, h) = self.placed[e].footprint;
341        let m = w.min(h);
342        let x1 = (w - 1 + m + x).min(n - 1);
343        let y1 = (h - 1 + m + y).min(n - 1);
344        let x0 = (x - m).max(0);
345        let y0 = y - m;
346        loop {
347            let Some(f) = self.fire(e) else { return };
348            if 0 < f.spread {
349                break;
350            }
351            f.spread += ticks(d.interval * PERIOD);
352            let cx = self.rng.rand_range(x0, x1);
353            let cy = self.rng.rand_range(y0, y1);
354            let cell = cy * n + cx;
355            // Below the map the original reads outside it; nothing burns there.
356            let other = (0 <= cell && cell < n * n)
357                .then(|| self.cells.occupants[cell as usize])
358                .flatten();
359            if let Some(o) = other {
360                if let Some(od) = self.fire_desc(o) {
361                    let mut chance = od.spread;
362                    if self.precipitation {
363                        chance *= 0.5;
364                    }
365                    let f = self.fire(e).unwrap();
366                    if od.buckets / 5 < f.buckets - f.buckets_seen {
367                        chance *= 0.5;
368                    }
369                    if self.rng.rand_float() < chance {
370                        self.ignite(jobs, o);
371                    }
372                }
373            }
374            if self.fire(e).is_none_or(|f| f.burn <= 0) {
375                break;
376            }
377        }
378        if let Some(f) = self.fire(e) {
379            f.buckets_seen = f.buckets;
380        }
381    }
382
383    /// `FUN_14008d6a0`: the nearest water within `max` (path rank, else `n` + distance) by
384    /// rings of 8 × 8 blocks around the citizen's: a shore point (its cell walkable) or a
385    /// well's first use point. A shore point gives (shore, water); a well a random use point
386    /// and its entrance.
387    fn water_search(&mut self, cell: i32, max: i32) -> Option<([f32; 3], [f32; 3])> {
388        let n = self.size as i32;
389        let b = n >> 3;
390        let (bx, by) = ((cell % n) >> 3, (cell / n) >> 3);
391        let mut best = max;
392        let mut found: Option<i32> = None;
393        let mut r = -1;
394        loop {
395            if best <= r.max(0) * 8 {
396                break;
397            }
398            let r1 = r + 1;
399            for dy in -r1..=r1 {
400                let y = by + dy;
401                if 0 <= y && y < b {
402                    if 0 <= bx - r1 && bx - r1 < b {
403                        self.water_block(y * b + bx - r1, -r1, dy, cell, &mut best, &mut found);
404                    }
405                    if 0 <= bx + r1 && bx + r1 < b {
406                        self.water_block(y * b + bx + r1, r1, dy, cell, &mut best, &mut found);
407                    }
408                }
409            }
410            for dx in 1 - r1..=r1 - 1 {
411                let x = bx + dx;
412                if 0 <= x && x < b {
413                    if 0 <= by - r1 && by - r1 < b {
414                        self.water_block((by - r1) * b + x, dx, -r1, cell, &mut best, &mut found);
415                    }
416                    if 0 <= by + r1 && by + r1 < b {
417                        self.water_block((by + r1) * b + x, dx, r1, cell, &mut best, &mut found);
418                    }
419                }
420            }
421            r = r1;
422        }
423        let v = found?;
424        if 0 <= v {
425            let (a, w) = self.water.points[v as usize];
426            return Some((self.ground_at(a), self.ground_at(w)));
427        }
428        let well = self.water.wells[(-1 - v) as usize]?;
429        let count = self.use_point_count(well);
430        let k = self.rng.rand_below(count as u32) as usize;
431        let point = self.use_point(well, k).0;
432        let entrance = self.entrance_cell(well);
433        Some((point, self.ground_at(entrance)))
434    }
435
436    /// `FUN_14008dc00`: the block's sources nearer than the best so far.
437    #[allow(clippy::too_many_arguments)]
438    fn water_block(
439        &mut self,
440        block: i32,
441        dx: i32,
442        dy: i32,
443        cell: i32,
444        best: &mut i32,
445        found: &mut Option<i32>,
446    ) {
447        let entries = self.water.blocks[block as usize].clone();
448        let d = (dy * 8) * (dy * 8) + (dx * 8) * (dx * 8);
449        for v in entries {
450            if *best * *best < d {
451                continue;
452            }
453            let target = if v < 0 {
454                let Some(well) = self.water.wells.get((-1 - v) as usize).copied().flatten() else {
455                    continue;
456                };
457                self.cell_of(self.use_point(well, 0).0)
458            } else {
459                let a = self.water.points[v as usize].0;
460                if (1u32 << (self.cells.types[a as usize].0 & 0x1f)) & WATER_MASK == 0 {
461                    continue;
462                }
463                a
464            };
465            if !self.reachable_by(WATER_MASK, cell, target) {
466                continue;
467            }
468            let rank = self.rank_or_distance(cell, target);
469            if rank < *best {
470                *best = rank;
471                *found = Some(v);
472            }
473        }
474    }
475
476    /// FireWork's slot `+0x20` (`FUN_1400eeaf0`): below its most firefighters, the nearest
477    /// water; then the burning building's use point nearest that water; within patience,
478    /// the bucket run.
479    pub(crate) fn fire_try(&mut self, work: &mut Work, c: usize, time: i64) -> u8 {
480        let e = work.entity;
481        let Some(d) = self.fire_desc(e) else { return 0 };
482        let WorkKind::Fire(fw) = &work.kind else {
483            return 0;
484        };
485        if d.workers <= fw.workers.len() as i32 {
486            return 1;
487        }
488        let cell = self.citizen_cell(c);
489        let Some((water, second)) = self.water_search(cell, self.rules.water_distance) else {
490            return 0;
491        };
492        // `FUN_140127fd0(interact, 0, ..)`: the use point nearest the water.
493        let from = self.cell_of(water);
494        let mut best = 0x7fffffff;
495        let mut point = None;
496        for &x in &self.interact_entities(e) {
497            for k in 0..self.templates[self.placed[x].template].use_points.len() {
498                let p = self.use_point(x, k).0;
499                let pc = self.cell_of(p);
500                if self.reachable(from, pc) {
501                    let r = self.rank_or_distance(from, pc);
502                    if r < best {
503                        best = r;
504                        point = Some(p);
505                    }
506                }
507            }
508        }
509        let Some(point) = point.filter(|_| best < 0xffff) else {
510            return 0;
511        };
512        let pc = self.cell_of(point);
513        if !self.patience(work, c, pc, cell, time) {
514            return 0;
515        }
516        self.fire_sequence(c, work, water, second, point, e);
517        self.took_job(c, work);
518        if let WorkKind::Fire(fw) = &mut work.kind {
519            fw.workers.insert(c);
520        }
521        2
522    }
523
524    /// `FUN_1400bfec0`: run to the water, fill the bucket, run to the fire, throw it (message
525    /// 0 to the FireComponent), done.
526    fn fire_sequence(
527        &mut self,
528        c: usize,
529        work: &mut Work,
530        water: [f32; 3],
531        second: [f32; 3],
532        point: [f32; 3],
533        e: usize,
534    ) {
535        self.clear_citizen(c, Some(work));
536        self.on_empty_think(c);
537        let facing = work_facing(self.facing_centre(e), point);
538        let heading = work_facing(second, water);
539        self.push(c, self.perform(c, facing, 7, THROW, false));
540        self.push(c, Action::ShowTool(-1));
541        self.push(
542            c,
543            Action::CompleteTask {
544                work: work.id,
545                param: 0,
546            },
547        );
548        self.push(
549            c,
550            Action::SendMessage(Message {
551                component: Component::Fire,
552                message: 0,
553                entity: Some(e),
554                a: 0,
555                b: 0,
556            }),
557        );
558        self.push(c, self.perform(c, facing, 6, THROW, false));
559        let run = MoveTo::new(point, 4, 0, 0xe1f, 0, RUN, self.rng);
560        self.push(c, Action::move_to(run));
561        self.push(c, self.perform(c, heading, 7, THROW, false));
562        self.push(c, Action::ShowTool(8));
563        self.push(c, self.perform(c, heading, 6, THROW, false));
564        let run = MoveTo::new(water, 1, 0, 0xe1f, 0, RUN, self.rng);
565        self.push(c, Action::move_to(run));
566    }
567
568    /// Well state 2 enter (`FUN_14005d880` → `FUN_140097180`) and state 3 enter
569    /// (`FUN_14005d840` → `FUN_140097070`).
570    pub(crate) fn well_join(&mut self, e: usize, join: bool) {
571        let cell = self.placed[e].cell;
572        if join {
573            self.water.add_well(e, cell);
574        } else {
575            self.water.remove_well(e, cell);
576        }
577    }
578}