Skip to main content

banished_game/disasters/
blight.rs

1//! Blight: DiseaseComponent on crop fields, orchards and pastures. See docs/FARMING.md "Blight".
2//!
3//! In its season a field may catch it, more likely when a cell grew the same thing last time. While
4//! sick it kills a crop, tree or animal now and then, and passes to fields nearby growing the same
5//! thing, until it runs its course or nothing is left.
6// vtable 0x14015f3b0
7
8use banished_formats::game::DiseaseDescription;
9
10use crate::nature::wildlife::HUNTED;
11use crate::scheduler::ticks;
12use crate::town::Sim;
13
14/// `DAT_140178934`.
15const PERIOD: f32 = 300.0;
16/// Event type 12: a field or pasture caught a disease.
17const CAUGHT: i32 = 0xc;
18// Game +0xd0
19/// `Game` bit 5: blight is off.
20pub const NO_BLIGHT: u32 = 0x20;
21
22// +0x28; +0x30; +0x38; +0x40; +0x48; +0x50; set up by FUN_1400495b0.
23/// The blight of a field, orchard or pasture: the time to the next check, the time left (sick while
24/// it is above zero), the time to the next death and to the next spread, whether it is on a
25/// pasture, and what grew when it last ended.
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct Blight {
28    /// The time to the next check.
29    pub check: i64,
30    /// The time left; sick while above zero.
31    pub left: i64,
32    /// The time to the next death.
33    pub kill: i64,
34    /// The time to the next spread.
35    pub spread: i64,
36    /// Whether it is on a pasture.
37    pub pasture: bool,
38    /// What grew when it last ended.
39    pub last: Option<usize>,
40}
41
42impl Sim<'_, '_> {
43    fn blight_desc(&self, e: usize) -> Option<DiseaseDescription> {
44        self.site_templates[self.sites.get(&e)?.template].disease
45    }
46
47    fn blight(&mut self, e: usize) -> Option<&mut Blight> {
48        self.sites.get_mut(&e)?.disease.as_mut()
49    }
50
51    /// `FUN_1400a50d0(map, rect, out, 0)`: the objects on the entity's cells, by rows.
52    fn objects_in(&self, e: usize) -> Vec<usize> {
53        let n = self.size as i32;
54        let (x, y) = self.placed[e].cell;
55        let (w, h) = self.placed[e].footprint;
56        let mut out = Vec::new();
57        for cy in y..y + h {
58            for cx in x..x + w {
59                if let Some(o) = self.cells.objects[(cy * n + cx) as usize] {
60                    out.push(o);
61                }
62            }
63        }
64        out
65    }
66
67    fn is_resource(&self, o: usize) -> bool {
68        self.placed[o].alive && self.templates[self.placed[o].template].natural.is_some()
69    }
70
71    /// `FUN_1400488e0`: what grows there: the pasture's kind; else the first natural resource
72    /// on its cells, or, with nothing on them, the field's crop or the orchard's tree.
73    fn grown(&self, e: usize) -> Option<usize> {
74        if let Some(p) = self.wildlife.pastures.get(&e) {
75            return p.kind;
76        }
77        let objects = self.objects_in(e);
78        if objects.is_empty() {
79            let site = self.sites.get(&e)?;
80            if let Some(f) = &site.crop_field {
81                return f.crop;
82            }
83            return site.orchard.as_ref().and_then(|o| o.tree);
84        }
85        objects
86            .into_iter()
87            .find(|&o| self.is_resource(o))
88            .map(|o| self.placed[o].template)
89    }
90
91    /// `FUN_140048b10`: nothing left to fall sick (an empty pasture, no natural resource on
92    /// the cells).
93    fn nothing_to_blight(&self, e: usize) -> bool {
94        if let Some(p) = self.wildlife.pastures.get(&e) {
95            return p.livestock.is_empty();
96        }
97        !self.objects_in(e).into_iter().any(|o| self.is_resource(o))
98    }
99
100    /// `FUN_140048d00`: a built field with something growing falls sick (event 12 at severity
101    /// 3, with the toolbar name). Its particles follow `left` in the app (docs/PARTICLES.md).
102    fn catch_blight(&mut self, e: usize) {
103        if self.nothing_to_blight(e) {
104            return;
105        }
106        // The state machine's pending state, else its state (`+0x52`, `+0x50`), past 1.
107        let state = self.sites.get(&e).map_or(0, |s| {
108            if s.machine.pending < 0 {
109                s.machine.state
110            } else {
111                s.machine.pending
112            }
113        });
114        if state <= 1 {
115            return;
116        }
117        let Some(d) = self.blight_desc(e) else { return };
118        self.site_events.push((CAUGHT, 3, e));
119        if let Some(b) = self.blight(e) {
120            b.left = ticks(d.duration * PERIOD);
121            b.kill = 0;
122            b.spread = 0;
123        }
124    }
125
126    /// `FUN_140048c80`: it is over; what grows now is remembered.
127    fn end_blight(&mut self, e: usize) {
128        let last = self.grown(e);
129        if let Some(b) = self.blight(e) {
130            b.check = 0;
131            b.left = 0;
132            b.kill = 0;
133            b.spread = 0;
134            b.last = last;
135        }
136    }
137
138    /// DiseaseComponent's scheduled update of every site that has one.
139    ///
140    /// Original: `FUN_1400494f0`.
141    pub fn update_blight(&mut self, visits: &[(usize, i64)]) {
142        for &(e, dt) in visits {
143            if !self.sites.get(&e).is_some_and(|s| s.disease.is_some()) {
144                continue;
145            }
146            let Some(d) = self.blight_desc(e) else {
147                continue;
148            };
149            let Some(left) = self.blight(e).map(|b| b.left) else {
150                continue;
151            };
152            if left < 1 {
153                self.blight_chance(e, &d, dt);
154                continue;
155            }
156            let b = self.blight(e).unwrap();
157            b.left -= dt;
158            b.kill += dt;
159            b.spread += dt;
160            if b.left < 0 || self.nothing_to_blight(e) {
161                self.end_blight(e);
162                continue;
163            }
164            let period = ticks(d.kill * PERIOD);
165            while 0 < period && period < self.blight(e).map_or(0, |b| b.kill) {
166                self.blight(e).unwrap().kill -= period;
167                if self.blight(e).is_some_and(|b| b.pasture) {
168                    self.blight_animal(e);
169                } else {
170                    self.blight_crop(e);
171                }
172            }
173            let period = ticks(d.spread * PERIOD);
174            while 0 < period && period < self.blight(e).map_or(0, |b| b.spread) {
175                self.blight(e).unwrap().spread -= period;
176                self.spread_blight(e, &d);
177            }
178        }
179    }
180
181    /// `FUN_140048f70`: in its months, a check every `check` × 300 s: odds of one in
182    /// `12 / check × odds`, or `odds_same` when a cell last grew what grew here.
183    /// Off with `Game +0xd0` bit 5 (no disasters, or the debug flag).
184    fn blight_chance(&mut self, e: usize, d: &DiseaseDescription, dt: i64) {
185        if self.game_flags & NO_BLIGHT != 0 {
186            return;
187        }
188        let mut month = (self.calendar / 3600.0 * 12.0) as i32;
189        if 11 < month {
190            month = 0;
191        }
192        if month < d.months.0 || d.months.1 < month {
193            return;
194        }
195        let period = ticks(d.check * PERIOD);
196        let Some(b) = self.blight(e) else { return };
197        b.check += dt;
198        let last = b.last;
199        while 0 < period && period < self.blight(e).map_or(0, |b| b.check) {
200            self.blight(e).unwrap().check -= period;
201            // `FUN_1400a5030`: a cell's last growth (`+0x10`) is what grew when it last ended.
202            let n = self.size as i32;
203            let (x, y) = self.placed[e].cell;
204            let (w, h) = self.placed[e].footprint;
205            let same = (x..x + w).any(|cx| {
206                (y..y + h).any(|cy| self.cells.previous_template[(cy * n + cx) as usize] == last)
207            });
208            let odds = if same { d.odds_same } else { d.odds };
209            let chance = 1.0 / ((12.0 / d.check) * odds);
210            if self.rng.rand_float() < chance {
211                self.catch_blight(e);
212            }
213        }
214    }
215
216    /// `FUN_1400486d0`: up to 50 draws for an animal no one has taken; it dies.
217    fn blight_animal(&mut self, e: usize) {
218        let Some(herd) = self.wildlife.pastures.get(&e).map(|p| p.livestock.clone()) else {
219            return;
220        };
221        if herd.is_empty() {
222            return;
223        }
224        for _ in 0..50 {
225            let a = herd[self.rng.rand_below(herd.len() as u32) as usize];
226            let Some(animal) = self.wildlife.animals.get(&a) else {
227                continue;
228            };
229            if animal.flags & HUNTED == 0 {
230                // Already going (entity flag `0x400`): nothing more.
231                if !animal.dying {
232                    self.wildlife.kill(a);
233                }
234                return;
235            }
236        }
237    }
238
239    /// `FUN_1400484a0`: up to 50 draws among the objects for a natural resource no one is
240    /// removing; it withers (no growth, no pile, removal with clip 2 when it has one).
241    fn blight_crop(&mut self, e: usize) {
242        let objects = self.objects_in(e);
243        if objects.is_empty() {
244            return;
245        }
246        for _ in 0..50 {
247            let o = objects[self.rng.rand_below(objects.len() as u32) as usize];
248            if self.is_resource(o) && !self.claimed.contains(&o) {
249                self.placed[o].growth = 0.0;
250                self.keep_piles.insert(o, false);
251                self.placed[o].removal_requested = true;
252                self.animate_resource(o, 2);
253                *self.models_dirty = true;
254                return;
255            }
256        }
257    }
258
259    /// The spread: a random cell within `radius` of the centre; the field or pasture there,
260    /// growing the same thing, falls sick.
261    fn spread_blight(&mut self, e: usize, d: &DiseaseDescription) {
262        let n = self.size as i32;
263        let (x, y) = self.placed[e].cell;
264        let (w, h) = self.placed[e].footprint;
265        let centre = ((h >> 1) + y) * n + (w >> 1) + x;
266        let cx = (centre % n + self.rng.rand_tol(d.radius)).max(0);
267        let cy = (centre / n + self.rng.rand_tol(d.radius)).max(0);
268        let (cx, cy) = (cx.min(n - 1), cy.min(n - 1));
269        let i = (cy * n + cx) as usize;
270        let Some(other) = self.cells.occupants[i].or(self.cells.objects[i]) else {
271            return;
272        };
273        if other == e || self.sites.get(&other).is_none_or(|s| s.disease.is_none()) {
274            return;
275        }
276        if self.grown(other) == self.grown(e) {
277            self.catch_blight(other);
278        }
279    }
280}