1use 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
20const PERIOD: f32 = 300.0;
22pub const LAYER: usize = 6;
25const MASK: u32 = 0x4000;
27const CAUGHT: i32 = 0x1f;
29pub const NO_FIRES: u32 = 0x40;
32const WATER_MASK: u32 = 0xe07;
34const THROW: f32 = 0.75;
36const RUN: f32 = 3.5;
37
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct Fire {
43 pub buckets: i32,
45 pub buckets_seen: i32,
47 pub burn: i64,
49 pub spread: i64,
51 pub timer: i64,
53}
54
55#[derive(Debug, Clone, Default)]
57pub struct Water {
58 pub points: Vec<(i32, i32)>,
61 pub wells: Vec<Option<usize>>,
64 pub blocks: Vec<Vec<i32>>,
67 pub size: i32,
69}
70
71impl Water {
72 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 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 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 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 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
160pub 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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}