Skip to main content

banished_game/work/
destroy.rs

1//! Demolition, damage and rubble: DestroyComponent. See docs/JOBS.md "Demolition".
2//!
3//! - 3: the work and status icons go; a damaged building goes on to 6, else to 4 with the
4//!   BuildDestroy icon;
5//! - 4: the building becomes a site again; a finished one with goods has them carried out first
6//!   (ClearStorage), then 5;
7//! - 5: builders take a started building down (a BuildStructure work with the destroy flag, half
8//!   its construction work); then it is gone, leaving part of its materials in piles, or what
9//!   replaces it;
10//! - 6: damaged (burning): the goods are lost; it collapses when its time runs out;
11//! - 7: rubble, to rebuild (message 0) or clear (message 1).
12//!
13//! Message 0 in 4 and 5 cancels the demolition.
14//!
15//! Original: `FUN_140034520`, `FUN_1401231d0`.
16// vtable 0x140169500
17
18use crate::town::{Sim, CONSTRUCTION_MASK};
19use crate::work::construction::{run, BuildWork, Callbacks, Controller};
20use crate::work::jobs::{Jobs, Work, WorkKind};
21use crate::work::professions::icon;
22
23/// A resolved [`banished_formats::game::DestroyDescription`].
24#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
25pub struct DestroyTemplate {
26    // +0x20
27    /// What is left in the building's place (a placed-object template).
28    pub replace: Option<usize>,
29    // +0x28
30    /// The ModelComponent's material set once damaged.
31    ///
32    /// Original: `FUN_14012a4f0`.
33    pub material: i32,
34    // +0x2c
35    /// The decal's material while damaged.
36    pub decal: i32,
37    // +0x30
38    /// The lowest construction mesh a collapse steps down to.
39    pub min_mesh: i32,
40}
41
42// +0x48; +0x50; +0x51; +0x54; +0x58; +0x60; +0x20
43// 0x68 bytes; set up by FUN_140122fd0 from the description.
44/// The demolition state of a building: the damage time left, whether it is damaged, whether its
45/// materials come back, the demolition work done and needed, and what to put in its place.
46#[derive(Debug, Clone, Default, PartialEq, Eq)]
47pub struct Destroy {
48    /// The damage time left.
49    pub time: i64,
50    /// Whether it is damaged.
51    pub damaged: bool,
52    /// Whether its materials come back.
53    pub refund: bool,
54    /// The demolition work done.
55    pub count: i32,
56    /// The demolition work needed.
57    pub target: i32,
58    /// The template to put in its place, if any.
59    pub rebuild: Option<usize>,
60    // +0x28
61    /// The model shows the description's damaged material set (set by the damage with time left and
62    /// by every collapse).
63    pub damaged_look: bool,
64}
65
66impl Destroy {
67    /// The demolition state of a new building.
68    pub fn new(t: &DestroyTemplate) -> Destroy {
69        Destroy {
70            rebuild: t.replace,
71            ..Default::default()
72        }
73    }
74}
75
76/// An entity put in a torn-down building's place: its template over the same map rectangle, turned
77/// as it was, with its model's position and variant.
78///
79/// Original: `FUN_1401215f0`.
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct Rebuild {
82    /// The template.
83    pub template: usize,
84    /// The map cell.
85    pub cell: (i32, i32),
86    /// The footprint.
87    pub footprint: (i32, i32),
88    /// The heading, in degrees.
89    pub yaw: f32,
90    /// The model's position.
91    pub position: [f32; 3],
92    /// The model variant.
93    pub variant: usize,
94}
95
96/// `DAT_140178904`, `DAT_1401787fc`.
97const QUARTER: f32 = 90.0;
98const FLAT: f32 = 0.2;
99
100impl Sim<'_, '_> {
101    fn destroy_state(&mut self, e: usize) -> Option<&mut Destroy> {
102        self.sites.get_mut(&e)?.destroy.as_mut()
103    }
104
105    /// The state machine's pending state, else its state (`+0x52`, `+0x50`).
106    fn shown_state(&self, e: usize) -> i8 {
107        self.sites.get(&e).map_or(-1, |s| {
108            if s.machine.pending < 0 {
109                s.machine.state
110            } else {
111                s.machine.pending
112            }
113        })
114    }
115
116    /// Damaged (6) or rubble (7).
117    ///
118    /// Original: `FUN_140123b60`.
119    pub fn is_damaged(&self, e: usize) -> bool {
120        matches!(self.shown_state(e), 6 | 7)
121    }
122
123    /// Being torn down (3 to 5).
124    ///
125    /// Original: `FUN_140123be0`.
126    pub fn is_demolishing(&self, e: usize) -> bool {
127        matches!(self.shown_state(e), 3..=5)
128    }
129
130    /// BuildComponent `+0x48 < BuildDescription +0x30 × +0x4c`: not finished.
131    fn unfinished(&self, e: usize) -> bool {
132        let site = &self.sites[&e];
133        let t = &self.site_templates[site.template];
134        site.progress < t.work * site.stages
135    }
136
137    /// A BuildComponent (removal markers have none).
138    pub(crate) fn has_build(&self, e: usize) -> bool {
139        let site = &self.sites[&e];
140        self.site_templates[site.template]
141            .controllers
142            .contains(&Controller::Build)
143    }
144
145    /// Remove Buildings and the demolish button. Damaged or rubble: clear it (message 1); else the
146    /// machine runs what is pending, then goes to 3.
147    ///
148    /// Original: `FUN_140123470`.
149    pub fn demolish(&mut self, jobs: &mut Jobs, e: usize) {
150        if self.destroy_state(e).is_none() {
151            return;
152        }
153        if self.is_damaged(e) {
154            self.destroy_message(jobs, e, 1);
155            return;
156        }
157        run(self, jobs, e, false, false);
158        self.machine(e).request(3);
159        run(self, jobs, e, false, false);
160    }
161
162    /// UpgradeUI's button: the replacement is `template`, and the building is torn down.
163    ///
164    /// Original: `FUN_14008c160`, `FUN_140123470`.
165    pub fn upgrade(&mut self, jobs: &mut Jobs, e: usize, template: usize) {
166        let Some(d) = self.destroy_state(e) else {
167            return;
168        };
169        d.rebuild = Some(template);
170        self.demolish(jobs, e);
171    }
172
173    // slot +0x20
174    /// DestroyUI's cancel (`0x1771`): message 0, and the replacement goes back to the
175    /// description's.
176    ///
177    /// Original: `FUN_1401238d0`.
178    pub fn cancel_demolition(&mut self, jobs: &mut Jobs, e: usize) {
179        self.destroy_message(jobs, e, 0);
180        let site = &self.sites[&e];
181        let replace = self.site_templates[site.template]
182            .destroy
183            .and_then(|d| d.replace);
184        if let Some(d) = self.destroy_state(e) {
185            d.rebuild = replace;
186        }
187    }
188
189    /// Damage for `time` (fire, the tornado). A building not started yet takes none; else it goes
190    /// to 3 (then 6).
191    ///
192    /// Original: `FUN_140123c80`.
193    pub fn damage(&mut self, jobs: &mut Jobs, e: usize, time: i64) -> bool {
194        if self.destroy_state(e).is_none() {
195            return false;
196        }
197        if self.has_build(e) && self.sites[&e].progress == 0 {
198            return false;
199        }
200        let d = self.destroy_state(e).unwrap();
201        d.damaged = true;
202        d.time = time;
203        if self.machine(e).index < 0 {
204            run(self, jobs, e, false, false);
205        }
206        self.machine(e).request(3);
207        run(self, jobs, e, false, false);
208        true
209    }
210
211    /// Straight to rubble.
212    ///
213    /// Original: `FUN_140123ee0`.
214    pub fn to_rubble(&mut self, jobs: &mut Jobs, e: usize) {
215        if self.machine(e).index < 0 {
216            run(self, jobs, e, false, false);
217        }
218        self.machine(e).request(7);
219        run(self, jobs, e, false, false);
220    }
221
222    // +0x10
223    /// The StateMachine's message slot for the Destroy states: the current state's handler.
224    pub fn destroy_message(&mut self, jobs: &mut Jobs, e: usize, msg: i32) {
225        if self.destroy_state(e).is_none() || self.machine(e).index < 0 {
226            return;
227        }
228        match (self.machine(e).state, msg) {
229            (4 | 5, 0) => self.cancel(jobs, e),
230            (6, 2) => self.machine(e).request(7),
231            // `FUN_140122770`: rebuild what stood here.
232            (7, 0) => {
233                self.become_site(jobs, e);
234                let template = self.placed[e].template;
235                let d = self.destroy_state(e).unwrap();
236                d.refund = true;
237                d.rebuild = Some(template);
238                self.machine(e).request(5);
239            }
240            // `FUN_140122670`: clear the rubble.
241            (7, 1) => {
242                self.become_site(jobs, e);
243                let build = self.has_build(e);
244                if build {
245                    self.destroy_state(e).unwrap().refund = true;
246                }
247                self.machine(e).request(5);
248            }
249            _ => {}
250        }
251    }
252
253    /// `FUN_140121190`: the demolition is called off. A building goes back to its finished
254    /// state (2), a site to building (8) or clearing (0, no mesh yet); a damaged one to rubble.
255    fn cancel(&mut self, jobs: &mut Jobs, e: usize) {
256        self.set_icon(e, icon::DESTROY, false);
257        self.remove_work(jobs, e);
258        if self.destroy_state(e).is_some_and(|d| d.damaged) {
259            self.become_building(jobs, e);
260            self.machine(e).request(7);
261            return;
262        }
263        if !self.has_build(e) {
264            self.machine(e).request(2);
265            run(self, jobs, e, false, false);
266        } else {
267            // The MultiObject's first part's model (`+0x166` mesh, `+0x164` variant).
268            let m = self.sites[&e].parts.first().copied().unwrap_or(e);
269            let mesh = self.placed[m].mesh;
270            let state = if mesh == usize::MAX {
271                0
272            } else if mesh as i32 == self.meshes_of(m) - 1 {
273                2
274            } else {
275                8
276            };
277            self.machine(e).request(state);
278        }
279        self.become_building(jobs, e);
280    }
281
282    fn meshes_of(&self, m: usize) -> i32 {
283        let p = &self.placed[m];
284        self.templates[p.template]
285            .model
286            .as_ref()
287            .and_then(|model| model.variants.get(p.variant))
288            .map_or(1, |v| v.meshes.len() as i32)
289    }
290
291    /// Residence state 3 enter (`FUN_14012edf0` → `FUN_14012f3c0`): the icons 6 and 7 go,
292    /// each family moves out (`FUN_14011b8a0(first, none, -1)`) and the residence leaves the
293    /// town's (`+0x20`). The ResidenceComponent is kept on the site meanwhile.
294    pub(crate) fn residence_leave(&mut self, e: usize) {
295        let Some(mut home) = self.homes.remove(&e) else {
296            return;
297        };
298        crate::people::status::set_icon(&mut home.status, icon::NO_FOOD, false, false);
299        crate::people::status::set_icon(&mut home.status, icon::NO_FIREWOOD, false, false);
300        for slot in 0..home.families.len() {
301            while let Some(&c) = home.families[slot].first() {
302                self.move_out(&mut home, c);
303            }
304        }
305        self.workforce.residences.retain(|&r| r != e);
306        if let Some(site) = self.sites.get_mut(&e) {
307            site.home = Some(home);
308        }
309    }
310
311    /// `FUN_14011b8a0` with no home: the citizen and its dependents leave theirs.
312    fn move_out(&mut self, home: &mut crate::people::life::Home, c: usize) {
313        for f in home.families.iter_mut() {
314            f.retain(|&x| x != c);
315        }
316        self.states[c].home = None;
317        self.states[c].home_slot = -1;
318        for d in self.states[c].dependents.clone() {
319            if self.states[d].home.is_some() {
320                self.move_out(home, d);
321            }
322        }
323    }
324
325    /// State 3 enter (`FUN_140122c80`).
326    pub(crate) fn destroy_enter3(&mut self, jobs: &mut Jobs, e: usize) {
327        if let Some(d) = self.destroy_state(e) {
328            d.count = 0;
329        }
330        let id = self.sites[&e].work_id;
331        if jobs.works.contains_key(&id) {
332            self.remove_work(jobs, e);
333        }
334        // `FUN_140131770`: every status icon goes.
335        if let Some(w) = self.workforce.workplaces.get_mut(&e) {
336            for bit in 0..32 {
337                w.set_icon(bit, false);
338            }
339        }
340        if self.destroy_state(e).is_some_and(|d| d.damaged) {
341            self.machine(e).request(6);
342        } else {
343            self.machine(e).request(4);
344            self.set_icon(e, icon::DESTROY, true);
345        }
346    }
347
348    /// State 4 enter (`FUN_1401224a0`): a site again; a finished building's goods go first.
349    pub(crate) fn destroy_enter4(&mut self, jobs: &mut Jobs, e: usize) {
350        self.become_site(jobs, e);
351        let build = self.has_build(e);
352        if build {
353            self.destroy_state(e).unwrap().refund = true;
354        }
355        if self.storage_empty(e) || (build && self.unfinished(e)) {
356            self.machine(e).request(5);
357            return;
358        }
359        let work = Work::new(
360            e,
361            self.rect_cell(e),
362            self.professions.laborer,
363            WorkKind::ClearStorage(crate::production::herbalist::HerbalistWork::default()),
364        );
365        self.set_work(jobs, e, work, 1, false);
366    }
367
368    /// `FUN_140122f50`.
369    fn storage_empty(&self, e: usize) -> bool {
370        let s = self.sites[&e].storage;
371        self.storages[s].used(self.materials) == 0
372    }
373
374    /// State 4 update (`FUN_140122390`): empty and nobody carrying from it (`FUN_140133ef0`,
375    /// the work's haulers): on to 5.
376    pub(crate) fn destroy_update4(&mut self, jobs: &mut Jobs, e: usize) {
377        if !self.storage_empty(e) {
378            return;
379        }
380        let id = self.sites[&e].work_id;
381        let busy = jobs.works.get(&id).is_some_and(|w| match &w.kind {
382            WorkKind::ClearStorage(c) => !c.workers.is_empty(),
383            _ => false,
384        });
385        if !busy {
386            self.machine(e).request(5);
387            self.remove_work(jobs, e);
388        }
389    }
390
391    /// State 5 enter (`FUN_140122140`): a started building is taken down by builders, half
392    /// its construction work (BuildStructure with the destroy flag, `FUN_1400eae90(.., 1)`);
393    /// one not started is gone at once.
394    pub(crate) fn destroy_enter5(&mut self, jobs: &mut Jobs, e: usize) {
395        if self.has_build(e) {
396            let site = &self.sites[&e];
397            if site.progress != 0 {
398                let work = self.site_templates[site.template].work;
399                let target = ((work * site.stages) / 2).max(1);
400                self.destroy_state(e).unwrap().target = target;
401                let w = Work::new(
402                    e,
403                    self.rect_cell(e),
404                    self.professions.builder,
405                    WorkKind::Build(BuildWork {
406                        destroy: true,
407                        occupied: vec![false; self.build_point_count(e)],
408                        ..Default::default()
409                    }),
410                );
411                self.set_work(jobs, e, w, CONSTRUCTION_MASK, false);
412                return;
413            }
414            // The materials come back before the entity goes (its zone still holds the
415            // footprint).
416            if self.destroy_state(e).is_some_and(|d| d.refund) {
417                let (x, y) = self.placed[e].cell;
418                let (w, h) = self.placed[e].footprint;
419                self.refund(e, x, y, w, h);
420            }
421        }
422        self.deactivate(jobs, e);
423    }
424
425    /// State 5 update (`FUN_1401215f0`): once the work is done the building goes; what
426    /// replaces it is placed, or its materials partly come back.
427    pub(crate) fn destroy_update5(&mut self, jobs: &mut Jobs, e: usize) {
428        let Some(d) = self.destroy_state(e).cloned() else {
429            return;
430        };
431        if d.count < d.target {
432            return;
433        }
434        self.remove_work(jobs, e);
435        let p = &self.placed[e];
436        let (cell, footprint, yaw) = (p.cell, p.footprint, p.yaw);
437        let (position, variant) = (p.position, p.variant);
438        self.become_building(jobs, e);
439        let finished = !self.unfinished(e);
440        self.deactivate(jobs, e);
441        if let Some(template) = d.rebuild {
442            if finished || d.damaged {
443                let own = self.placed[e].template;
444                let replace = self.site_templates[self.sites[&e].template]
445                    .destroy
446                    .and_then(|t| t.replace);
447                // `FUN_140123ad0`, `FUN_140123a80`: a rebuild or another replacement than the
448                // description's gives the materials back too.
449                if d.refund && (template == own || Some(template) != replace) {
450                    self.refund(e, cell.0, cell.1, footprint.0, footprint.1);
451                }
452                let mut yaw = yaw as i32;
453                while yaw < 0 {
454                    yaw += 360;
455                }
456                self.rebuilds.push(Rebuild {
457                    template,
458                    cell,
459                    footprint,
460                    yaw: yaw as f32,
461                    position,
462                    variant,
463                });
464                return;
465            }
466        }
467        if d.refund {
468            self.refund(e, cell.0, cell.1, footprint.0, footprint.1);
469        }
470    }
471
472    /// State 6 enter (`FUN_1401229a0`): the goods are lost; with no time left it collapses.
473    pub(crate) fn destroy_enter6(&mut self, e: usize) {
474        let s = self.sites[&e].storage;
475        // `FUN_14008f300`: out of the town's storages.
476        self.storage_changes.push((e, false));
477        self.storages[s].items.clear();
478        if self.destroy_state(e).is_some_and(|d| d.time < 1) {
479            self.collapse(e);
480        } else {
481            // `FUN_14012a4f0(model, +0x28)`.
482            self.damaged_look(e);
483        }
484    }
485
486    /// State 6 update (`FUN_140122960`).
487    pub(crate) fn destroy_update6(&mut self, e: usize, dt: i64) {
488        let Some(d) = self.destroy_state(e) else {
489            return;
490        };
491        if d.time != 0 {
492            d.time -= dt;
493            if d.time < 1 {
494                d.time = 0;
495                self.collapse(e);
496            }
497        }
498    }
499
500    /// State 6 exit (`FUN_1401228d0`): with time left it collapses, and one time in two it
501    /// collapses (again).
502    pub(crate) fn destroy_exit6(&mut self, e: usize) {
503        if self.templates[self.placed[e].template].model.is_none() {
504            return;
505        }
506        if self.destroy_state(e).is_some_and(|d| 0 < d.time) {
507            self.collapse(e);
508        }
509        if self.rng.rand_below(2) == 0 {
510            self.collapse(e);
511        }
512    }
513
514    /// The model's damaged material set (`FUN_14012a4f0` with the description's `+0x28`).
515    fn damaged_look(&mut self, e: usize) {
516        if let Some(d) = self.destroy_state(e) {
517            if !d.damaged_look {
518                d.damaged_look = true;
519                *self.models_dirty = true;
520            }
521        }
522    }
523
524    /// `FUN_140122aa0`: one construction mesh down, not below the description's `+0x30`; the
525    /// model takes the damaged material set and the decal the damaged material. Its entity
526    /// event 0 reaches only the HighlightComponent (`FUN_14004ce00`, the outline of the new
527    /// mesh) and `FUN_14012e420` re-registers the picking mesh; the port's highlight and
528    /// picking read the entity's current mesh, so both follow.
529    fn collapse(&mut self, e: usize) {
530        let Some(t) = self
531            .sites
532            .get(&e)
533            .and_then(|s| self.site_templates[s.template].destroy)
534        else {
535            return;
536        };
537        let mesh = self.placed[e].mesh;
538        let current = if mesh == usize::MAX { -1 } else { mesh as i32 };
539        let next = (current - 1).max(0).max(t.min_mesh);
540        self.damaged_look(e);
541        let d = self.decals.entry(e).or_default();
542        if d.material != t.decal {
543            d.material = t.decal;
544            *self.models_dirty = true;
545        }
546        if next != current {
547            self.placed[e].mesh = next as usize;
548            *self.models_dirty = true;
549        }
550    }
551
552    /// `FUN_1400c8370` on an active entity: each component's deactivate slot runs now (the
553    /// zone and the map stamp are lifted); the entity is removed after the machine has run.
554    pub(crate) fn deactivate(&mut self, jobs: &mut Jobs, e: usize) {
555        self.remove_work(jobs, e);
556        let n = self.size as i32;
557        let stamped = {
558            let p = &self.placed[e];
559            let (x, y) = p.cell;
560            let (w, h) = p.footprint;
561            (y..y + h).any(|cy| {
562                (x..x + w).any(|cx| self.cells.objects[(cy * n + cx) as usize] == Some(e))
563            })
564        };
565        if stamped {
566            self.unstamp(e);
567        }
568        let p = &self.placed[e];
569        let (x, y) = p.cell;
570        let (w, h) = p.footprint;
571        for cy in y..y + h {
572            for cx in x..x + w {
573                let i = (cy * n + cx) as usize;
574                if self.cells.occupants[i] == Some(e) {
575                    self.cells.occupants[i] = None;
576                }
577            }
578        }
579        if let Some(site) = self.sites.get_mut(&e) {
580            site.destroyed = true;
581        }
582    }
583
584    /// `FUN_140121a00`: piles of the materials over the rectangle (`x`, `y`, `w`, `h`), by rows,
585    /// on free flat ground: an unfinished building's stored materials (half once work
586    /// began), else half its build materials (a quarter when damaged); at most `100 / size`
587    /// a pile, each turned a random quarter.
588    fn refund(&mut self, e: usize, x: i32, y: i32, w: i32, h: i32) {
589        let site = &self.sites[&e];
590        let t = &self.site_templates[site.template];
591        let mut piles: Vec<(usize, i32)> = Vec::new();
592        if site.progress < t.work * site.stages {
593            let div = if 0 < site.progress { 2 } else { 1 };
594            for item in &self.storages[site.storage].items {
595                let Some(k) = t.materials.iter().position(|&(_, m)| m == item.material) else {
596                    continue;
597                };
598                if let Some(&Some(pile)) = t.material_templates.get(k) {
599                    piles.push((pile, (item.count - item.reserved) / div));
600                }
601            }
602        } else {
603            let div = if self.sites[&e].destroy.as_ref().is_some_and(|d| d.damaged) {
604                4
605            } else {
606                2
607            };
608            for (k, &(count, _)) in t.materials.iter().enumerate() {
609                if let Some(&Some(pile)) = t.material_templates.get(k) {
610                    piles.push((pile, count / div));
611                }
612            }
613        }
614        let n = self.size as i32;
615        let (mut col, mut row) = (0, 0);
616        for (pile, mut left) in piles {
617            if left <= 0 {
618                continue;
619            }
620            let Some(material) = self.templates[pile].pile_material else {
621                continue;
622            };
623            // The cursor carries on from the last material's, and the scan checks the last
624            // row after the cell (a material after the rectangle is full scans on below it).
625            loop {
626                let cell = (y + row) * n + x + col;
627                if cell < 0 || n * n <= cell {
628                    break;
629                }
630                let i = cell as usize;
631                let ground = self.ground_at(cell);
632                if self.cells.occupants[i].is_none()
633                    && self.cells.types[i].0 == 2
634                    && 0.0 <= ground[2]
635                    && ground[2] < FLAT
636                {
637                    let amount = left.min(100 / self.materials[material].size.max(1));
638                    let yaw = self.rng.rand_tol(2) as f32 * QUARTER;
639                    self.spawn_pile(pile, (x + col, y + row), amount, yaw, None);
640                    left -= amount;
641                }
642                col += 1;
643                if w <= col {
644                    col = 0;
645                    row += 1;
646                }
647                if row == h || left <= 0 {
648                    break;
649                }
650            }
651        }
652    }
653}