banished_game/disasters/
blight.rs1use banished_formats::game::DiseaseDescription;
9
10use crate::nature::wildlife::HUNTED;
11use crate::scheduler::ticks;
12use crate::town::Sim;
13
14const PERIOD: f32 = 300.0;
16const CAUGHT: i32 = 0xc;
18pub const NO_BLIGHT: u32 = 0x20;
21
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
27pub struct Blight {
28 pub check: i64,
30 pub left: i64,
32 pub kill: i64,
34 pub spread: i64,
36 pub pasture: bool,
38 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 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 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 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 fn catch_blight(&mut self, e: usize) {
103 if self.nothing_to_blight(e) {
104 return;
105 }
106 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 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 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 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 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 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 if !animal.dying {
232 self.wildlife.kill(a);
233 }
234 return;
235 }
236 }
237 }
238
239 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 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}