Skip to main content

banished_game/people/
life.rs

1//! The lives of citizens and their homes: needs, age, death, health, happiness, disease, homes,
2//! partners and births. See [Citizens](https://tanepiper.github.io/unbanished/topics/citizens),
3//! docs/CITIZEN-NEEDS-AND-HOUSEHOLDS.md and docs/CITIZEN-LIFE.md.
4//!
5//! The citizen description is kept as a table by offset, so the port reads the same values as
6//! the original.
7//!
8//! Deviations: the entity references the original keeps (home, partner, guardian, dependents)
9//! are citizen and entity indices; the dependents' ring is a vector in ring order. Disease
10//! spread visits the citizens within the radius in registration order where the original asks
11//! its spatial index.
12//!
13//! Original: `CitizenComponent` (vtable `0x1401689a0`), `ResidenceComponent` (vtable
14//! `0x14016a6a0`); disease spread `FUN_140117940`.
15// The CitizenDescription is at citizen +0x288.
16
17use std::collections::BTreeMap;
18
19use banished_formats::game::{CitizenDescription, Disease, ResidenceDescription};
20
21use crate::behaviour::activities::TownView;
22use crate::behaviour::agents::CitizenState;
23use crate::pathfind::WALKABLE;
24use crate::people::citizens::Citizen;
25use crate::random::Random;
26use crate::scheduler::{seconds, ticks};
27use crate::work::stock::{RawMaterial, Storage};
28
29/// `DAT_140178934`: the game's "minute" (most description times are in these units).
30const MINUTE: f32 = 300.0;
31/// `DAT_14017895c`.
32const HOUR: f32 = 3600.0;
33
34/// The CitizenDescription values, by offset.
35#[derive(Debug, Clone, Default)]
36pub struct LifeRules {
37    fields: BTreeMap<u32, f32>,
38    ints: BTreeMap<u32, i32>,
39    // D+0x20; D+0x28; D+0x30
40    /// `D`, `D`, `D`: the teacher's, priest's and doctor's professions (the workplaces whose
41    /// attendance a citizen joins), as profession indices.
42    pub services: [Option<usize>; 3],
43}
44
45// D+0x20/+0x28/+0x30
46/// Which service an attendance search is for (`D/`).
47pub mod service {
48    /// A school.
49    pub const SCHOOL: usize = 0;
50    /// A chapel.
51    pub const CHAPEL: usize = 1;
52    /// A doctor.
53    pub const DOCTOR: usize = 2;
54}
55
56impl LifeRules {
57    /// The rules from the citizen description.
58    pub fn new(d: &CitizenDescription) -> LifeRules {
59        let mut r = LifeRules::default();
60        // Constructor values (`FUN_14011a340`) of the fields the resource does not store:
61        // illness length `+0x94` / `+0x98` and immunity `+0x9c`, in minutes.
62        for (o, v) in [(0x94, 1.0), (0x98, 0.5), (0x9c, 12.0)] {
63            r.fields.insert(o, v);
64            r.ints.insert(o, v as i32);
65        }
66        for &o in d.fields.keys() {
67            r.fields.insert(o, d.f(o));
68            r.ints.insert(o, d.i(o));
69        }
70        r
71    }
72
73    /// The float at `D + o`.
74    pub fn f(&self, o: u32) -> f32 {
75        self.fields.get(&o).copied().unwrap_or(0.0)
76    }
77
78    /// The integer at `D + o`.
79    pub fn i(&self, o: u32) -> i32 {
80        self.ints.get(&o).copied().unwrap_or(0)
81    }
82
83    /// The age at which a child becomes an adult.
84    pub fn adult_age(&self) -> f32 {
85        self.f(0x44)
86    }
87
88    /// `(D[0xd0] − D[0xcc]) · ½ + D[0xcc]`: hungry enough for the status icon and a health event.
89    pub fn hunger_warning(&self) -> f32 {
90        (self.f(0xd0) - self.f(0xcc)) * 0.5 + self.f(0xcc)
91    }
92
93    /// `(D[0xf0] − D[0xec]) · ¼ + D[0xec]`.
94    pub fn cold_warning(&self) -> f32 {
95        (self.f(0xf0) - self.f(0xec)) * 0.25 + self.f(0xec)
96    }
97}
98
99/// The causes of death and the life events, as the event log's types.
100///
101/// Original: the reasons of `FUN_14011c2a0`.
102pub mod reason {
103    /// Starved.
104    pub const STARVED: i32 = 0;
105    /// Froze.
106    pub const FROZE: i32 = 1;
107    /// Died of old age.
108    pub const OLD_AGE: i32 = 2;
109    /// Died of disease.
110    pub const DISEASE: i32 = 3;
111    /// A child whose single guardian died.
112    ///
113    /// Original: `FUN_140119170`.
114    pub const ORPHANED: i32 = 4;
115    /// Killed by a tornado.
116    ///
117    /// Original: `FUN_140058d20`.
118    pub const TORNADO: i32 = 5;
119    /// Died in childbirth.
120    pub const CHILDBIRTH: i32 = 6;
121    // +0x1f8
122    /// Citizen: an accident (fire, tornado) named by its description.
123    pub const ACCIDENT: i32 = 7;
124    /// Was born.
125    pub const BORN: i32 = 8;
126    /// Became an adult.
127    pub const ADULT: i32 = 9;
128    /// Became a student.
129    pub const STUDENT: i32 = 10;
130    /// A laborer took the dead citizen's profession.
131    ///
132    /// Original: `FUN_140119170`.
133    pub const REPLACED: i32 = 0xf;
134    /// No laborer took the dead citizen's profession.
135    pub const NOT_REPLACED: i32 = 0x10;
136}
137
138/// An event the citizens' update asks the log for.
139///
140/// Original: `FUN_140118fc0`.
141#[derive(Debug, Clone, PartialEq)]
142pub struct LifeEvent {
143    /// The event type.
144    pub kind: i32,
145    /// How important the event is.
146    pub severity: i32,
147    /// The citizen.
148    pub citizen: usize,
149    /// `@4`: a disease's or accident's name (string-table key and table).
150    pub extra: Option<(Option<String>, String)>,
151}
152
153/// ResidenceComponent: the family slots and the fire.
154#[derive(Debug, Clone, PartialEq)]
155pub struct Home {
156    /// The residence description.
157    pub desc: ResidenceDescription,
158    /// The site's StorageComponent.
159    pub storage: usize,
160    // +0x50
161    /// The residents of each family slot, sorted as the original sorts them.
162    ///
163    /// Original: sorted by `FUN_14012f2f0`.
164    pub families: Vec<Vec<usize>>,
165    // +0x48
166    /// Time the last firewood keeps burning.
167    pub fuel: i64,
168    /// The StatusIconComponent bits the residence sets: 6 no food, 7 no firewood.
169    pub status: u32,
170}
171
172impl Home {
173    /// A home with its description and its storage.
174    pub fn new(desc: ResidenceDescription, storage: usize) -> Home {
175        Home {
176            desc,
177            storage,
178            families: vec![Vec::new(); desc.families.max(1) as usize],
179            fuel: 0,
180            status: 0,
181        }
182    }
183
184    /// The number of residents.
185    ///
186    /// Original: `FUN_14012f4c0`.
187    pub fn occupants(&self) -> usize {
188        self.families.iter().map(Vec::len).sum()
189    }
190
191    // +0x24
192    /// Family `slot` has at least residents.
193    ///
194    /// Original: `FUN_14012f9c0`.
195    pub fn full(&self, slot: usize) -> bool {
196        self.families
197            .get(slot)
198            .is_some_and(|f| f.len() as i32 >= self.desc.family_size)
199    }
200
201    /// Warm enough outside, or the fire is burning.
202    ///
203    /// Original: `FUN_140130100`.
204    pub fn warm(&self, temperature: f32) -> bool {
205        self.desc.warm_temperature <= temperature || self.fuel > 0
206    }
207
208    /// The number of families with residents.
209    ///
210    /// Original: counted by `FUN_14011abb0` and `FUN_14011acc0`.
211    pub fn families_living(&self) -> i32 {
212        self.families.iter().filter(|f| !f.is_empty()).count() as i32
213    }
214}
215
216/// The units of a category in `storage` that nobody has reserved: food (bit 5) or firewood
217/// (bit 6).
218///
219/// Original: `FUN_14012fea0` (food), `FUN_14012ffd0` (firewood).
220pub fn stored(storage: &Storage, materials: &[RawMaterial], bit: u32) -> i32 {
221    storage
222        .items
223        .iter()
224        .filter(|i| materials[i.material].flags >> bit & 1 != 0)
225        .map(|i| i.count - i.reserved)
226        .sum()
227}
228
229/// `FUN_14012fbe0`'s sort (`FUN_14012f2f0`): citizens without a guardian first, then by sex.
230pub(crate) fn family_order(citizens: &[Citizen], a: usize, b: usize) -> std::cmp::Ordering {
231    let key = |c: usize| {
232        let c = &citizens[c];
233        (c.parent.is_some() as u32) << 12 | (c.male as u32) << 8
234    };
235    key(a).cmp(&key(b))
236}
237
238/// Everything one citizen's update touches.
239pub struct People<'a, 'b> {
240    /// The citizens.
241    pub citizens: &'a mut Vec<Citizen>,
242    /// The citizens' states.
243    pub states: &'a mut Vec<CitizenState>,
244    // +0xa0
245    /// The living citizens, in activation order.
246    pub registry: &'a mut Vec<usize>,
247    /// The homes, by entity.
248    pub homes: &'a mut BTreeMap<usize, Home>,
249    /// The citizen description's rules.
250    pub rules: &'a LifeRules,
251    /// A view of the town.
252    pub town: &'a TownView<'b>,
253    // +0x1c..+0x27
254    /// Map cells ([`crate::nature::seeding::Cells::layers`]).
255    pub layers: &'a [[u8; 12]],
256    /// The game's random numbers.
257    pub rng: &'a mut Random,
258    /// The environment's temperature.
259    pub temperature: f32,
260    /// The diseases.
261    pub diseases: &'a [Disease],
262    // +0x1a8
263    /// The extra disease risk that visitors bring (0 until they are ported).
264    pub town_disease: f32,
265    /// The town's storages.
266    pub storages: &'a mut [Storage],
267    /// The raw materials.
268    pub materials: &'a [RawMaterial],
269    /// The character's action stack is empty (`stack_top + 1 < 1`).
270    pub idle: &'a [bool],
271    /// The log events asked for.
272    pub events: &'a mut Vec<LifeEvent>,
273    /// Citizens whose entity was queued for deletion, with the reason.
274    ///
275    /// Original: `FUN_1400c8370`.
276    pub dying: &'a mut Vec<(usize, i32)>,
277    /// Makes a child of `mother`: name, sex, appearance and lifespan, with their random draws.
278    /// The caller of this closure adds it to `citizens`.
279    ///
280    /// Original: `FUN_14011c550`, called from `FUN_140117060`.
281    pub newborn: &'a mut dyn FnMut(&mut Random, &Citizen, bool) -> Citizen,
282    // (the flag is the child's sex, `true` male)
283    /// Children born this update (new citizen indices).
284    pub born: &'a mut Vec<usize>,
285    // +0x60
286    /// The AttendanceComponents, by entity.
287    pub attendance: &'a mut BTreeMap<usize, crate::work::construction::Attendance>,
288    /// The service buildings that people attend.
289    pub attendance_list: &'a [usize],
290    // +0x100; +0x20
291    /// The workplaces and residences.
292    pub workforce: &'a mut crate::work::professions::Workforce,
293    /// The professions.
294    pub professions: &'a crate::work::professions::Professions,
295    /// The sites (their works' priorities, a cemetery's graves).
296    pub sites: &'a BTreeMap<usize, crate::work::construction::Site>,
297    // +0x40
298    /// The town's cemeteries.
299    pub cemeteries: &'a [usize],
300    /// The tombstones standing, with the time each one stays.
301    pub tombstones: &'a BTreeMap<usize, i64>,
302    /// Burials decided this update (cemetery, grave cell); the tombstones follow.
303    pub burials: &'a mut Vec<(usize, i32)>,
304    /// The path-rank cache.
305    pub sectors: &'a mut crate::sectors::SectorGraph,
306    /// The ground's heights.
307    pub terrain: &'a crate::terrain::HeightGrid<'b>,
308    // Game +0x88
309    /// The characters' tree (`Game`): a disease spreads to the citizens about the sick.
310    pub tree: &'a crate::behaviour::character_tree::CharacterTree,
311}
312
313impl People<'_, '_> {
314    fn adult(&self, c: usize) -> bool {
315        self.citizens[c].age >= self.rules.adult_age()
316    }
317
318    fn cell(&self, c: usize) -> i32 {
319        self.town.cell_of(self.citizens[c].position)
320    }
321
322    fn dying(&self, c: usize) -> bool {
323        self.dying.iter().any(|&(d, _)| d == c)
324    }
325
326    /// `FUN_140118fc0`: a log entry naming the citizen and its role.
327    fn report(
328        &mut self,
329        c: usize,
330        kind: i32,
331        severity: i32,
332        extra: Option<(Option<String>, String)>,
333    ) {
334        self.events.push(LifeEvent {
335            kind,
336            severity,
337            citizen: c,
338            extra,
339        });
340    }
341
342    /// `FUN_140118340`'s workplace search: without a workplace, or when `FUN_140115eb0` says
343    /// to look again, `FUN_140115450` chooses one (or none) and `FUN_1401173b0` moves there.
344    fn search_workplace(&mut self, c: usize) {
345        let town = self.town;
346        let citizens = &*self.citizens;
347        let cell = |i: usize| town.cell_of(citizens[i].position);
348        let professions = self.professions;
349        let anywhere = |p: usize| professions.works_anywhere(p);
350        let sites = self.sites;
351        let priority = |e: usize| {
352            sites
353                .get(&e)
354                .and_then(|s| s.work_priority)
355                .unwrap_or(i32::MAX)
356        };
357        let mut search = crate::behaviour::activities::TownSearch {
358            town,
359            cell: &cell,
360            anywhere: &anywhere,
361            priority: &priority,
362            rng: &mut *self.rng,
363        };
364        let (p, current) = (self.states[c].profession, self.states[c].workplace);
365        if current.is_none() || self.workforce.should_move(c, p, current, &mut search) {
366            let wp = self.workforce.find_workplace(c, p, &mut search);
367            if wp != current {
368                self.workforce.move_worker(c, current, wp);
369                self.states[c].workplace = wp;
370            }
371        }
372    }
373
374    /// The home's entrance is reachable from the citizen's cell.
375    ///
376    /// Original: `FUN_14011ada0`.
377    pub fn reaches(&mut self, c: usize, entity: usize) -> bool {
378        let cell = self.cell(c);
379        self.town.can_reach(cell, entity, self.rng)
380    }
381
382    /// Move citizen `c` (and its dependents) to `home` family `slot`.
383    ///
384    /// Original: `FUN_14011b8a0`.
385    pub fn set_home(&mut self, c: usize, home: Option<usize>, slot: i32) {
386        let s = &self.states[c];
387        if s.home == home && s.home_slot == slot {
388            return;
389        }
390        if let Some(old) = s.home {
391            let old_slot = s.home_slot;
392            self.remove_resident(old, old_slot, c);
393        }
394        self.states[c].home = home;
395        self.states[c].home_slot = slot;
396        if let Some(new) = home {
397            self.add_resident(new, slot, c);
398        }
399        for d in self.states[c].dependents.clone() {
400            self.set_home(d, home, slot);
401        }
402    }
403
404    /// `FUN_14012fbe0`: append and sort the family; the first resident lights the house.
405    fn add_resident(&mut self, home: usize, slot: i32, c: usize) {
406        let citizens = &*self.citizens;
407        if let Some(h) = self.homes.get_mut(&home) {
408            if let Some(f) = h.families.get_mut(slot.max(0) as usize) {
409                f.push(c);
410                f.sort_by(|&a, &b| family_order(citizens, a, b));
411            }
412        }
413    }
414
415    /// `FUN_14012fa20`: remove; the last resident leaving clears the status icons 6 and 7.
416    fn remove_resident(&mut self, home: usize, slot: i32, c: usize) {
417        if let Some(h) = self.homes.get_mut(&home) {
418            if let Some(f) = h.families.get_mut(slot.max(0) as usize) {
419                if let Some(i) = f.iter().position(|&x| x == c) {
420                    f.remove(i);
421                }
422            }
423            if h.occupants() == 0 {
424                crate::people::status::set_icon(&mut h.status, 6, false, false);
425                crate::people::status::set_icon(&mut h.status, 7, false, false);
426            }
427        }
428    }
429
430    /// Make `guardian` the guardian of `c`.
431    ///
432    /// Original: `FUN_140117470`.
433    pub fn set_guardian(&mut self, c: usize, guardian: Option<usize>) {
434        if self.citizens[c].parent == guardian {
435            return;
436        }
437        if let Some(g) = self.citizens[c].parent {
438            self.remove_dependent(g, c);
439        }
440        if let Some(g) = guardian {
441            self.add_dependent(g, c);
442        }
443    }
444
445    /// `FUN_14011bd70`: join `g`'s ring (at its end) and its home.
446    fn add_dependent(&mut self, g: usize, c: usize) {
447        self.states[g].dependents.push(c);
448        self.citizens[c].parent = Some(g);
449        let (home, slot) = (self.states[g].home, self.states[g].home_slot);
450        self.set_home(c, home, slot);
451    }
452
453    /// `FUN_140117510`: leave `g`'s ring; homeless and without a guardian.
454    fn remove_dependent(&mut self, g: usize, c: usize) {
455        self.states[g].dependents.retain(|&d| d != c);
456        self.set_home(c, None, -1);
457        self.citizens[c].parent = None;
458    }
459
460    /// The path-rank distance (`FUN_140008830`).
461    fn rank(&mut self, a: i32, b: i32) -> i32 {
462        let (size, terrain, floors) = (self.town.size, self.terrain, self.town.floors);
463        let ground = move |cell: i32| crate::town::floor_ground(floors, size, terrain, cell);
464        let mut g = crate::behaviour::activities::StorageGeometry {
465            town: self.town,
466            sectors: self.sectors,
467            ground: &ground,
468        };
469        crate::work::stock::StorageSearchMap::rank(&mut g, a, b)
470    }
471
472    /// `FUN_140044a50`: room, a worker, and the building reachable from the citizen.
473    fn accepts(&self, e: usize, c: usize) -> bool {
474        let Some(a) = self.attendance.get(&e) else {
475            return false;
476        };
477        if a.attendees.len() as i32 >= a.capacity {
478            return false;
479        }
480        if self
481            .workforce
482            .workplaces
483            .get(&e)
484            .is_none_or(|w| w.workers.is_empty())
485        {
486            return false;
487        }
488        let to = self.town.point_cell(e);
489        to >= 0 && self.town.reachable(WALKABLE, self.cell(c), to)
490    }
491
492    /// `FUN_140116e00`: the accepting attendance of `service` nearest the home (path rank
493    /// between entrances; any one without a home), skipping `exclude`.
494    fn find_service(&mut self, c: usize, service: usize, exclude: Option<usize>) -> Option<usize> {
495        let profession = self.rules.services[service]?;
496        let home = self.states[c]
497            .home
498            .map_or(-1, |h| self.town.entrance(h, self.rng));
499        let mut best = -1;
500        let mut found = None;
501        for &e in self.attendance_list {
502            // The workplace's description profession (`FUN_140116e00` compares `+0x48`).
503            if Some(e) == exclude
504                || self.workforce.workplaces.get(&e).and_then(|w| w.profession) != Some(profession)
505                || !self.accepts(e, c)
506            {
507                continue;
508            }
509            let entrance = self.town.entrance(e, self.rng);
510            let d = if home >= 0 {
511                self.rank(entrance, home)
512            } else {
513                1
514            };
515            if best < 0 || d < best {
516                best = d;
517                found = Some(e);
518            }
519        }
520        found
521    }
522
523    // +0x220
524    /// Join the nearest school, chapel or doctor; the school also sets.
525    ///
526    /// Original: `FUN_140116d10`, `FUN_140116bc0`, `FUN_140116a60`.
527    pub fn join_service(&mut self, c: usize, service: usize, exclude: Option<usize>) -> bool {
528        let Some(e) = self.find_service(c, service, exclude) else {
529            return false;
530        };
531        if let Some(a) = self.attendance.get_mut(&e) {
532            a.attendees.push(c);
533        }
534        let s = &mut self.states[c];
535        match service {
536            service::SCHOOL => {
537                s.school = Some(e);
538                s.schooled = true;
539            }
540            service::CHAPEL => s.priest = Some(e),
541            _ => s.doctor = Some(e),
542        }
543        true
544    }
545
546    /// Leave; the school can be swapped for another at once (`again`).
547    ///
548    /// Original: `FUN_14011af20`, `FUN_140116b20`, `FUN_14011ae80`.
549    pub fn leave_service(&mut self, c: usize, service: usize, again: bool) {
550        let slot = match service {
551            service::SCHOOL => self.states[c].school.take(),
552            service::CHAPEL => self.states[c].priest.take(),
553            _ => self.states[c].doctor.take(),
554        };
555        let Some(e) = slot else { return };
556        if let Some(a) = self.attendance.get_mut(&e) {
557            a.attendees.retain(|&x| x != c);
558        }
559        if again && service == service::SCHOOL && !self.join_service(c, service, None) {
560            self.states[c].school = None;
561        }
562    }
563
564    /// Citizen message 4: a lesson; at `D[0x118]` lessons, or after `D[0x11c]` hours at school,
565    /// educated (flag `0x400`), out of school and an adult (9).
566    ///
567    /// Original: `FUN_140116c70`.
568    pub fn lesson(&mut self, c: usize) {
569        let r = self.rules;
570        let s = &mut self.states[c];
571        self.citizens[c].education += 1;
572        if self.citizens[c].education < r.i(0x118) && seconds(s.school_time) <= r.f(0x11c) * HOUR {
573            return;
574        }
575        s.flags |= 0x400;
576        self.citizens[c].education = r.i(0x118);
577        self.citizens[c].educated = true;
578        self.leave_service(c, service::SCHOOL, false);
579        self.report(c, reason::ADULT, 0, None);
580    }
581
582    /// Citizen message 5: the doctor shortens the illness by `D[0x12c]` minutes.
583    ///
584    /// Original: `FUN_140116a10`.
585    pub fn treated(&mut self, c: usize) {
586        if self.states[c].disease.is_some() {
587            self.states[c].disease_left -= ticks(self.rules.f(0x12c) * MINUTE);
588        }
589    }
590
591    // Game +0xd0
592    /// Queue the citizen's death (`reason`) and tell the family. When the citizen is buried in a
593    /// cemetery (found from the home's entrance, else from where the citizen stands), the
594    /// family's bit is `0x40`; otherwise it is `2` for old age, else `0x80`. Not ported: the debug
595    /// flag that stops deaths (`Game` bit 8).
596    ///
597    /// Original: `FUN_14011c2a0`; the cemetery search is `FUN_14008ec10`.
598    pub fn die(&mut self, c: usize, reason: i32, extra: Option<(Option<String>, String)>) -> bool {
599        if self.dying(c) {
600            return true;
601        }
602        let from = match self.states[c].home {
603            Some(h) => self.town.point_cell(h),
604            None => self.cell(c),
605        };
606        let bit = match self.find_cemetery(from) {
607            Some(cemetery) => {
608                if let Some(slot) = self.free_grave(cemetery) {
609                    let cell = self.sites[&cemetery].graves[slot];
610                    self.burials.push((cemetery, cell));
611                }
612                0x40
613            }
614            None if reason == reason::OLD_AGE => 2,
615            None => 0x80,
616        };
617        if let Some(g) = self.citizens[c].parent {
618            self.states[g].flags |= bit;
619        }
620        if let Some(s) = self.citizens[c].spouse {
621            self.states[s].flags |= bit;
622            for d in self.states[s].dependents.clone() {
623                self.states[d].flags |= bit;
624            }
625        }
626        for d in self.states[c].dependents.clone() {
627            self.states[d].flags |= bit;
628        }
629        self.report(c, reason, 1, extra);
630        self.dying.push((c, reason));
631        true
632    }
633
634    /// `FUN_14008ec10`: the cemetery nearest `cell` by path rank (or `n + distance`) whose
635    /// use point can be reached and which has a free grave.
636    fn find_cemetery(&mut self, cell: i32) -> Option<usize> {
637        let mut best = None;
638        let mut nearest = i32::MAX;
639        let n = self.town.size as i32;
640        for &e in self.cemeteries {
641            let to = self.town.point_cell(e);
642            let mut rank = self.rank(cell, to);
643            if rank < 0 {
644                let (dx, dy) = ((cell % n - to % n) as f32, (cell / n - to / n) as f32);
645                rank = n + (dx * dx + dy * dy).sqrt() as i32;
646            }
647            if rank < nearest
648                && self.town.reachable(WALKABLE, cell, to)
649                && self.free_grave(e).is_some()
650            {
651                best = Some(e);
652                nearest = rank;
653            }
654        }
655        best
656    }
657
658    /// `FUN_1400475c0`: from a random grave on, the first without a tombstone.
659    fn free_grave(&mut self, cemetery: usize) -> Option<usize> {
660        let graves = &self.sites.get(&cemetery)?.graves;
661        let count = graves.len();
662        let start = self.rng.rand_below(count as u32) as usize;
663        (0..count).map(|i| (start + i) % count).find(|&i| {
664            let cell = graves[i];
665            let taken = self.town.objects[cell as usize]
666                .is_some_and(|o| self.tombstones.contains_key(&o))
667                || self.burials.iter().any(|&(_, b)| b == cell);
668            !taken
669        })
670    }
671
672    /// The family part of a citizen's removal: the citizen leaves school, the doctor, the chapel,
673    /// the home and the guardian. A widow or widower takes the dependents; otherwise children die
674    /// (orphaned) and older dependents leave. The partner is single again.
675    ///
676    /// Original: `FUN_140119170` (the removal of a `CitizenComponent`).
677    pub fn bury(&mut self, c: usize) {
678        self.leave_service(c, service::SCHOOL, false);
679        self.leave_service(c, service::DOCTOR, false);
680        self.leave_service(c, service::CHAPEL, false);
681        self.set_home(c, None, -1);
682        if let Some(g) = self.citizens[c].parent {
683            self.remove_dependent(g, c);
684        }
685        match self.citizens[c].spouse {
686            None => {
687                let adult = self.rules.adult_age();
688                for d in self.states[c].dependents.clone() {
689                    if self.citizens[d].age <= adult {
690                        self.die(d, reason::ORPHANED, None);
691                    } else {
692                        self.set_guardian(d, None);
693                        self.set_home(d, None, -1);
694                        self.leave_service(d, service::SCHOOL, false);
695                    }
696                }
697            }
698            Some(p) => {
699                for d in self.states[c].dependents.clone() {
700                    self.set_guardian(d, Some(p));
701                }
702            }
703        }
704        if let Some(p) = self.citizens[c].spouse.take() {
705            self.citizens[p].spouse = None;
706        }
707        self.registry.retain(|&x| x != c);
708    }
709
710    /// One scheduled update of citizen `c` over `dt`.
711    ///
712    /// Original: `FUN_140118340`.
713    pub fn update(&mut self, c: usize, dt: i64) {
714        let r = self.rules;
715        let secs = seconds(dt);
716        let adult_age = r.adult_age();
717        let was_child = self.citizens[c].age < adult_age || self.states[c].school.is_some();
718        let age = secs * r.f(0x40) + self.citizens[c].age;
719        self.citizens[c].age = age;
720        {
721            let s = &mut self.states[c];
722            s.health_timer += dt;
723            s.happiness_timer += dt;
724            s.clothing_remaining -= dt;
725            if s.school.is_some() {
726                s.school_time += dt;
727            }
728            s.disease_clock += dt;
729            s.accident_timer += dt;
730            if s.clothing_remaining < 0 {
731                s.clothing = 0;
732                s.clothing_remaining = 0;
733            }
734        }
735        // Coming of age: a school place (code 10) or adulthood (code 9).
736        if was_child && adult_age <= age && self.states[c].school.is_none() {
737            let student = !self.states[c].schooled && self.join_service(c, service::SCHOOL, None);
738            self.report(
739                c,
740                if student {
741                    reason::STUDENT
742                } else {
743                    reason::ADULT
744                },
745                0,
746                None,
747            );
748        }
749        let s = &mut self.states[c];
750        if s.sick && s.doctor.is_some() {
751            s.hunger = 0.0;
752            s.cold = 0.0;
753        }
754        s.hunger += secs * r.f(0xc0) / HOUR;
755        if r.f(0xf4) <= self.temperature {
756            s.cold = 0.0;
757        } else {
758            let rate = r.f(0xdc + 4 * s.clothing.clamp(0, 2) as u32);
759            s.cold += secs * rate / MINUTE;
760        }
761        // An accident at work (`+0x1f8`, `FUN_14011bb20`): code 7 with the profession's
762        // death message.
763        if let Some(p) = self.states[c].accident {
764            let extra = self
765                .professions
766                .list
767                .get(p)
768                .map(|p| (p.string_table.clone(), p.death.clone()));
769            if self.die(c, reason::ACCIDENT, extra) {
770                return;
771            }
772        }
773        let s = &mut self.states[c];
774        if r.f(0xd0) <= s.hunger {
775            s.hunger = r.f(0xd0);
776            if self.idle[c] && self.die(c, reason::STARVED, None) {
777                return;
778            }
779        }
780        let s = &mut self.states[c];
781        if r.f(0xf0) <= s.cold {
782            s.cold = r.f(0xf0);
783            if self.idle[c] && self.die(c, reason::FROZE, None) {
784                return;
785            }
786        }
787        if self.citizens[c].lifespan <= self.citizens[c].age && self.die(c, reason::OLD_AGE, None) {
788            return;
789        }
790        if !self.disease(c, dt) {
791            let extra = self.states[c]
792                .disease
793                .and_then(|d| self.diseases.get(d))
794                .map(|d| (d.table.clone(), d.name.clone()));
795            if self.die(c, reason::DISEASE, extra) {
796                return;
797            }
798        }
799        if self.states[c].disease.is_some() {
800            self.states[c].flags |= 1;
801            if self.states[c].doctor.is_none() {
802                self.join_service(c, service::DOCTOR, None);
803            }
804        }
805        if self.states[c].priest.is_none() {
806            self.join_service(c, service::CHAPEL, None);
807        }
808        let warn_hunger = r.hunger_warning();
809        let warn_cold = r.cold_warning();
810        {
811            let s = &mut self.states[c];
812            crate::people::status::set_icon(&mut s.status, 0, s.home.is_none(), false);
813            crate::people::status::set_icon(&mut s.status, 1, warn_hunger < s.hunger, false);
814            crate::people::status::set_icon(&mut s.status, 2, warn_cold < s.cold, false);
815            // Icon 3: no workplace, for a profession that does not work anywhere
816            // (profession `+0x4c` clear, citizen `+0x248` unset).
817            let unplaced = !self.professions.works_anywhere(s.profession) && s.workplace.is_none();
818            crate::people::status::set_icon(&mut s.status, 3, unplaced, false);
819            crate::people::status::set_icon(&mut s.status, 5, s.disease.is_some(), false);
820            if warn_hunger < s.hunger {
821                s.health_events |= 1;
822                s.flags |= 1;
823            }
824            if warn_cold < s.cold {
825                s.health_events |= 2;
826                s.flags |= 1;
827            }
828        }
829        let interval = ticks(r.f(0x78) * MINUTE);
830        while interval > 0 && interval < self.states[c].health_timer {
831            self.states[c].health_timer -= interval;
832            self.health(c);
833        }
834        let interval = ticks(r.f(0xa0) * MINUTE);
835        while interval > 0 && interval < self.states[c].happiness_timer {
836            self.states[c].happiness_timer -= interval;
837            self.happiness(c);
838        }
839        // A home that cannot be reached is given up.
840        if self.idle[c] {
841            if let Some(home) = self.states[c].home {
842                if !self.reaches(c, home) {
843                    self.set_home(c, None, -1);
844                }
845            }
846        }
847        let child = self.citizens[c].age < adult_age || self.states[c].school.is_some();
848        // A child without a home follows its guardian's.
849        if child && self.idle[c] && self.states[c].home.is_none() {
850            if let Some(g) = self.citizens[c].parent {
851                if let Some(home) = self.states[g].home {
852                    if self.reaches(c, home) {
853                        let slot = self.states[g].home_slot;
854                        self.set_home(c, Some(home), slot);
855                    }
856                }
857            }
858        }
859        if !child && self.idle[c] {
860            self.search_workplace(c);
861            if self.states[c].home.is_none() || self.citizens[c].parent.is_some() {
862                if self.states[c].home.is_none() {
863                    self.states[c].flags |= 0x20;
864                }
865                let homeless = self.states[c].home.is_none();
866                if let Some((home, slot)) = self.find_home(c, homeless) {
867                    if self.citizens[c].parent.is_some() {
868                        self.states[c].flags |= 0x2000;
869                    }
870                    self.set_guardian(c, None);
871                    self.set_home(c, Some(home), slot);
872                    if let Some(p) = self.citizens[c].spouse {
873                        self.set_home(p, Some(home), slot);
874                    }
875                }
876            }
877            // A family in a shared residence looks for a house of its own.
878            if let Some(home) = self.states[c].home {
879                if self.homes.get(&home).is_some_and(|h| h.families.len() > 1) {
880                    if let Some((home, slot)) = self.find_home(c, false) {
881                        self.set_home(c, Some(home), slot);
882                        if let Some(p) = self.citizens[c].spouse {
883                            self.set_home(p, Some(home), slot);
884                        }
885                    }
886                }
887            }
888            // Marriage.
889            if self.citizens[c].parent.is_none()
890                && self.states[c].home.is_some()
891                && self.citizens[c].spouse.is_none()
892                && r.f(0x48) <= self.citizens[c].age
893            {
894                if let Some(p) = self.find_partner(c) {
895                    self.citizens[c].spouse = Some(p);
896                    self.states[c].flags |= 0x200;
897                    self.states[p].flags |= 0x200;
898                    self.set_guardian(p, None);
899                    self.citizens[p].spouse = Some(c);
900                    let (home, slot) = (self.states[c].home, self.states[c].home_slot);
901                    self.set_home(p, home, slot);
902                }
903            }
904        }
905        self.pregnancy(c, secs);
906    }
907
908    /// The birth part of `FUN_140118340`.
909    fn pregnancy(&mut self, c: usize, secs: f32) {
910        let r = self.rules;
911        if self.citizens[c].male
912            || self.citizens[c].age < r.adult_age()
913            || self.states[c].school.is_some()
914        {
915            return;
916        }
917        let s = &mut self.states[c];
918        if (r.hunger_warning() < s.hunger || r.cold_warning() < s.cold) && 0.0 < s.reproduction {
919            s.reproduction = 0.0;
920        }
921        let t = s.reproduction;
922        if t <= 0.0 {
923            if t < 0.0 {
924                s.reproduction = t + secs;
925                if 0.0 <= t + secs {
926                    s.reproduction = 0.0;
927                }
928            }
929        } else {
930            s.reproduction = t - secs;
931            if t - secs <= 0.0 {
932                let health = s.health;
933                let threshold = (health / r.f(0x70)) * (r.f(0x64) - r.f(0x68)) + r.f(0x68);
934                if threshold <= self.rng.rand_float() {
935                    self.give_birth(c);
936                    let cooldown = self.rng.rand_float() * r.f(0x60) + r.f(0x5c);
937                    self.states[c].reproduction = -cooldown;
938                } else if self.die(c, reason::CHILDBIRTH, None) {
939                    return;
940                }
941            }
942        }
943        let s = &self.states[c];
944        if self.citizens[c].spouse.is_some() && s.home.is_some() && s.reproduction == 0.0 {
945            let age = self.citizens[c].age;
946            if r.f(0x50) <= age && age <= r.f(0x54) {
947                let full = s
948                    .home
949                    .and_then(|h| self.homes.get(&h))
950                    .is_some_and(|h| h.full(s.home_slot.max(0) as usize));
951                if !full {
952                    self.states[c].reproduction = r.f(0x58);
953                }
954            }
955        }
956    }
957
958    /// `FUN_140117060`: a child, its sex chosen to even out the unmarried young.
959    fn give_birth(&mut self, mother: usize) {
960        let r = self.rules;
961        // The unmarried no older than `D[0x4c]` and the boys among them: a boy is likelier the
962        // fewer boys there are.
963        let (mut young, mut boys) = (0, 0);
964        for &i in self.registry.iter() {
965            if self.citizens[i].spouse.is_none() && self.citizens[i].age <= r.f(0x4c) {
966                if self.citizens[i].male {
967                    boys += 1;
968                }
969                young += 1;
970            }
971        }
972        let male = if young != 0 {
973            let x = self.rng.rand_float();
974            boys as f32 / young as f32 <= x
975        } else {
976            self.rng.rand_below(2) == 1
977        };
978        let mut child = (self.newborn)(self.rng, &self.citizens[mother], male);
979        child.position = self.citizens[mother].position;
980        child.age = 0.0;
981        let id = self.citizens.len();
982        self.citizens.push(child);
983        self.states.push(CitizenState::newborn(r));
984        self.add_dependent(mother, id);
985        self.registry.push(id);
986        self.born.push(id);
987        self.report(id, reason::BORN, 0, None);
988        self.states[mother].flags |= 0x100;
989        if let Some(f) = self.citizens[mother].spouse {
990            self.states[f].flags |= 0x100;
991        }
992    }
993
994    /// `FUN_140115310`: the first registered single of the other sex old enough to marry and
995    /// close enough in age.
996    fn find_partner(&self, c: usize) -> Option<usize> {
997        let r = self.rules;
998        let me = &self.citizens[c];
999        self.registry.iter().copied().find(|&i| {
1000            let o = &self.citizens[i];
1001            o.male != me.male
1002                && o.spouse.is_none()
1003                && r.f(0x48) < o.age
1004                && (o.age - me.age).abs() <= r.f(0x4c)
1005                && !self.dying(i)
1006        })
1007    }
1008
1009    /// `FUN_140116340`: a home for `c`. A partner's single-family home, unless an adult in a
1010    /// shared home or homeless waits for one; else the first free family slot of a
1011    /// single-family residence reachable from `c`, else (with `shared`) the first shared one.
1012    fn find_home(&mut self, c: usize, shared: bool) -> Option<(usize, i32)> {
1013        if let Some(p) = self.citizens[c].spouse {
1014            if let Some(home) = self.states[p].home {
1015                if self.reaches(c, home)
1016                    && self.homes.get(&home).is_some_and(|h| h.families.len() < 2)
1017                {
1018                    return Some((home, self.states[p].home_slot));
1019                }
1020            }
1021        }
1022        if let Some(mine) = self.states[c].home {
1023            // Moving out of a shared residence waits while another adult is homeless, or (when
1024            // this citizen's home is single-family) lives in a shared one.
1025            let single = self.homes.get(&mine).is_some_and(|h| h.families.len() == 1);
1026            let adult = self.rules.adult_age();
1027            for &o in self.registry.iter() {
1028                if o == c || self.citizens[o].age < adult || self.states[o].school.is_some() {
1029                    continue;
1030                }
1031                let Some(h) = self.states[o].home else {
1032                    return None;
1033                };
1034                if single && self.homes.get(&h).is_some_and(|h| h.families.len() > 1) {
1035                    return None;
1036                }
1037            }
1038        }
1039        let cell = self.cell(c);
1040        let mut fallback = None;
1041        for e in self.workforce.residences.clone() {
1042            let Some(home) = self.homes.get(&e) else {
1043                continue;
1044            };
1045            if !shared && home.families.len() >= 2 {
1046                continue;
1047            }
1048            for (slot, family) in home.families.iter().enumerate() {
1049                if !family.is_empty() {
1050                    continue;
1051                }
1052                let entrance = self.town.point_cell(e);
1053                if entrance < 0 || !self.town.reachable(WALKABLE, cell, entrance) {
1054                    continue;
1055                }
1056                if home.families.len() == 1 {
1057                    return Some((e, slot as i32));
1058                }
1059                if fallback.is_none() {
1060                    fallback = Some((e, slot as i32));
1061                }
1062            }
1063        }
1064        fallback
1065    }
1066
1067    /// `FUN_140117e60`: health from the last interval's events.
1068    fn health(&mut self, c: usize) {
1069        let r = self.rules;
1070        let adult = self.adult(c) && self.states[c].school.is_none();
1071        let s = &mut self.states[c];
1072        let e = s.health_events;
1073        let mut n = (e >> 8 & 1) + (e >> 6 & 1) + (e >> 5 & 1) + (e >> 4 & 1);
1074        if e & 0x200 != 0 {
1075            n += 1;
1076        }
1077        if e & 8 != 0 && e & 4 != 0 && 3 < n {
1078            s.health += 0.5;
1079        }
1080        if e & 8 != 0 && e & 0x200 == 0 && n < 3 {
1081            s.health -= 1.0 - n as f32 / 3.0;
1082        }
1083        if e & 1 != 0 {
1084            s.health -= 1.0;
1085        }
1086        if e & 2 != 0 {
1087            s.health -= 1.0;
1088        }
1089        if e & 4 == 0 && adult {
1090            s.health -= 0.25;
1091        }
1092        if s.health < 0.0 {
1093            s.health = 0.0;
1094        }
1095        s.health = s.health.min(r.f(0x70));
1096        s.health_events = 0;
1097    }
1098
1099    /// `FUN_140117fb0`: happiness from events, needs, food variety, the home's surroundings
1100    /// and the chapel.
1101    fn happiness(&mut self, c: usize) {
1102        let r = self.rules;
1103        let home_bonus = self.states[c].home.map(|h| {
1104            // The home entrance cell's layers 0..=4 that are nonzero, less layer 5 (the
1105            // HappinessComponent radii, `FUN_1400a4e60`), at least 0.
1106            let cell = self.town.entrance(h, self.rng);
1107            let l = usize::try_from(cell)
1108                .ok()
1109                .and_then(|i| self.layers.get(i))
1110                .copied()
1111                .unwrap_or_default();
1112            let v = (l[..5].iter().filter(|&&b| b != 0).count() as i32 - l[5] as i32).max(0);
1113            if self.homes.get(&h).is_some_and(|h| h.families.len() > 1) {
1114                v >> 2
1115            } else {
1116                v
1117            }
1118        });
1119        let s = &mut self.states[c];
1120        let f = s.flags;
1121        let mut h = s.happiness;
1122        if f & 2 != 0 {
1123            h -= 8.0;
1124        }
1125        if f & 0x40 != 0 {
1126            h -= 2.0;
1127        }
1128        if f & 0x80 != 0 {
1129            h -= 4.0;
1130        }
1131        if f & 8 != 0 {
1132            h -= 1.0;
1133        }
1134        if f & 0x10 != 0 {
1135            h -= 1.0;
1136        }
1137        for bit in [8, 9, 10, 11, 12, 13] {
1138            if f >> bit & 1 != 0 {
1139                h += 1.0;
1140            }
1141        }
1142        if f >> 0x12 & 1 != 0 {
1143            h += 0.5;
1144        }
1145        h = h.max(0.0).min(r.f(0xa4));
1146        let mut target = 0.0;
1147        if f & 4 == 0 {
1148            target += 1.0;
1149        }
1150        if f & 0x20 == 0 {
1151            target += 1.0;
1152        }
1153        if f & 1 == 0 {
1154            target += 1.0;
1155        }
1156        if f >> 0x11 & 1 != 0 {
1157            // Citizen `+0x84` against `_workForHappiness` (`+0xb0`, 10): nothing in 1.0.7
1158            // writes `+0x84` (no store in the Application but the zeroed component), so the
1159            // original gives the half point every time, as here.
1160            target += if s.field_84 < r.i(0xb0) { 0.5 } else { 1.0 };
1161        }
1162        if f >> 0x10 & 1 != 0 {
1163            let foods = s.foods.len() as i32;
1164            if r.i(0xa8) < foods {
1165                if r.i(0xac) <= foods {
1166                    target += 1.0;
1167                }
1168            } else {
1169                target -= 1.0;
1170            }
1171        }
1172        if let Some(b) = home_bonus {
1173            target += b as f32;
1174        }
1175        if s.priest.is_some() {
1176            target += 1.0;
1177        }
1178        let target = target * 0.125 * r.f(0xa4);
1179        if h < target {
1180            h += 0.5;
1181        } else if target < h {
1182            // Through a double (`DAT_140178868` is 0.5 as a double).
1183            h = (h as f64 - 0.5) as f32;
1184        }
1185        s.happiness = h.max(0.0).min(r.f(0xa4));
1186    }
1187
1188    /// `FUN_140117ca0`: false when the disease kills the citizen.
1189    fn disease(&mut self, c: usize, dt: i64) -> bool {
1190        let r = self.rules;
1191        match self.states[c].disease {
1192            None => {
1193                let s = &mut self.states[c];
1194                s.immunity = (s.immunity - dt).max(0);
1195                let weak = 1.0 - s.health / r.f(0x70);
1196                let a = (r.f(0x84) - r.f(0x7c)) * weak + r.f(0x7c);
1197                let b = (r.f(0x88) - r.f(0x80)) * weak + r.f(0x80);
1198                let a = (r.f(0x8c) - a) * self.town_disease + a;
1199                let b = (r.f(0x90) - b) * self.town_disease + b;
1200                // Odds of 0 or less clear the clock first; the draw is made either way.
1201                if b <= 0.0 {
1202                    s.disease_clock = 0;
1203                }
1204                let elapsed = s.disease_clock;
1205                s.disease_clock = 0;
1206                let span = a * 12.0 * MINUTE * b;
1207                let x = self.rng.rand_float();
1208                if x < seconds(elapsed) / span {
1209                    self.outbreak(c);
1210                }
1211                true
1212            }
1213            Some(d) => {
1214                let s = &mut self.states[c];
1215                s.disease_left -= dt;
1216                s.spread -= dt;
1217                if s.spread < 1 {
1218                    self.spread(c);
1219                    let interval = self.diseases.get(d).map_or(0.0, |d| d.spread_interval);
1220                    self.states[c].spread = ticks(interval * MINUTE);
1221                }
1222                if self.states[c].disease_left < 1 {
1223                    return self.recover(c);
1224                }
1225                true
1226            }
1227        }
1228    }
1229
1230    /// `FUN_140117af0`: unless someone is already sick, a disease picked by weight.
1231    fn outbreak(&mut self, c: usize) {
1232        if self
1233            .registry
1234            .iter()
1235            .any(|&i| self.states[i].disease.is_some())
1236        {
1237            return;
1238        }
1239        let mut total = 0.0;
1240        let cumulative: Vec<f32> = self
1241            .diseases
1242            .iter()
1243            .map(|d| {
1244                total += d.weight;
1245                total
1246            })
1247            .collect();
1248        let x = self.rng.rand_float() * total;
1249        let mut low = 0.0;
1250        for (i, &high) in cumulative.iter().enumerate() {
1251            if low <= x && x < high {
1252                self.infect(c, i);
1253                break;
1254            }
1255            low = high;
1256        }
1257    }
1258
1259    /// `FUN_14011c060`: catch disease `d` (not while immune, nor at work in a profession
1260    /// with `+0x4d`, the doctor). The outbreak event asks AutoSlow through the event log.
1261    fn infect(&mut self, c: usize, d: usize) {
1262        let s = &self.states[c];
1263        if s.disease.is_some() || 0 < s.immunity {
1264            return;
1265        }
1266        let at_work = s.workplace.is_some()
1267            && self
1268                .professions
1269                .list
1270                .get(s.profession)
1271                .is_some_and(|p| p.immune_at_work);
1272        if at_work {
1273            return;
1274        }
1275        let first = !self
1276            .registry
1277            .iter()
1278            .any(|&i| self.states[i].disease.is_some());
1279        if first {
1280            let extra = self
1281                .diseases
1282                .get(d)
1283                .map(|d| (d.table.clone(), d.name.clone()));
1284            self.report(c, 0xb, 3, extra);
1285        }
1286        let r = self.rules;
1287        let x = self.rng.rand_float_tol();
1288        let s = &mut self.states[c];
1289        s.disease = Some(d);
1290        s.sick = true;
1291        s.disease_left = ticks((x * r.f(0x98) + r.f(0x94)) * MINUTE);
1292        s.spread = ticks(self.diseases.get(d).map_or(0.0, |d| d.spread_interval) * MINUTE);
1293        self.join_service(c, service::DOCTOR, None);
1294    }
1295
1296    /// `FUN_140117940`: with the disease's odds, every other citizen within its radius of the
1297    /// sick one (`FUN_140097e60` with the CitizenComponent type, in the characters' tree's
1298    /// order) who is not sick catches it (`FUN_14011c060`, which turns away the immune).
1299    fn spread(&mut self, c: usize) {
1300        let Some(d) = self.states[c].disease else {
1301            return;
1302        };
1303        let Some(disease) = self.diseases.get(d).cloned() else {
1304            return;
1305        };
1306        let x = self.rng.rand_float();
1307        if disease.spread_chance < x {
1308            return;
1309        }
1310        let p = self.citizens[c].position;
1311        let near: Vec<usize> = self
1312            .tree
1313            .within(p, disease.spread_radius, |w| {
1314                matches!(w, crate::behaviour::character_tree::Who::Citizen(_))
1315            })
1316            .into_iter()
1317            .filter_map(|w| match w {
1318                crate::behaviour::character_tree::Who::Citizen(i) if i != c => Some(i),
1319                _ => None,
1320            })
1321            .collect();
1322        for i in near {
1323            if self.states.get(i).is_some_and(|s| s.disease.is_none()) {
1324                self.infect(i, d);
1325            }
1326        }
1327    }
1328
1329    /// `FUN_140117880`: when the illness ends, recover (immune for `D[0x9c]` minutes) or die.
1330    fn recover(&mut self, c: usize) -> bool {
1331        let r = self.rules;
1332        let x = self.rng.rand_float();
1333        let s = &self.states[c];
1334        let doctor = if s.doctor.is_some() { r.f(0x128) } else { 1.0 };
1335        let recovery = s
1336            .disease
1337            .and_then(|d| self.diseases.get(d))
1338            .map_or(1.0, |d| d.recovery);
1339        let lives = 1.0 / recovery / doctor < x;
1340        if lives {
1341            let s = &mut self.states[c];
1342            s.spread = 0;
1343            s.immunity = ticks(r.f(0x9c) * MINUTE);
1344            s.disease = None;
1345            s.sick = false;
1346        }
1347        // `FUN_14011ae80`: leave the doctor.
1348        self.leave_service(c, service::DOCTOR, false);
1349        lives
1350    }
1351}
1352
1353/// Citizen message 0: warm up at `home` if it is warm.
1354///
1355/// Original: `FUN_1401176b0`.
1356pub fn warm_at(s: &mut CitizenState, home: Option<&Home>, temperature: f32) {
1357    if home.is_some_and(|h| h.warm(temperature)) {
1358        s.cold = 0.0;
1359    }
1360}
1361
1362// +0xc
1363/// Citizen message 2: eat from `storage` until the citizen is no longer hungry: one unit of food
1364/// per point of hunger. Each kind of food is remembered (for variety) and counts as used.
1365///
1366/// Original: `FUN_14011a7b0`, `FUN_14008e460`.
1367pub fn eat(
1368    s: &mut CitizenState,
1369    storage: &mut Storage,
1370    materials: &[RawMaterial],
1371    rng: &mut Random,
1372    counters: &mut crate::work::tracker::StockCounters,
1373) {
1374    while 0.0 < s.hunger {
1375        let Some(m) = storage.find_item(materials, 0, rng) else {
1376            return;
1377        };
1378        s.flags |= 0x10000;
1379        if !s.foods.contains(&m) {
1380            s.foods.push(m);
1381        }
1382        storage.remove(m, 1);
1383        let flags = materials[m].flags;
1384        if let Some(slot) = crate::work::produce::production_group(flags) {
1385            counters.used[slot] += 1;
1386        }
1387        if s.health_events & 8 == 0 {
1388            s.health_events = s.health_events & 0xfffffe8f | 8;
1389        }
1390        for (bit, event) in [(0, 0x10), (1, 0x20), (2, 0x40), (4, 0x100)] {
1391            if flags >> bit & 1 != 0 {
1392                s.health_events |= event;
1393            }
1394        }
1395        s.hunger -= 1.0;
1396        if s.hunger < 0.0 {
1397            s.hunger = 0.0;
1398            return;
1399        }
1400    }
1401}
1402
1403// +0xc; +0x2c
1404/// A residence's update. The fire burns down; with residents, while it is colder than the house's
1405/// warmth, one unit of firewood keeps it burning for seconds. The food and firewood status icons
1406/// clear when there is some. The ClearStorage work comes first
1407/// ([`crate::town::Sim::residence_clear_storage`]). The firewood burned counts as used in the stock
1408/// manager.
1409///
1410/// Original: `FUN_14012ee40`.
1411pub fn update_home(
1412    home: &mut Home,
1413    storages: &mut [Storage],
1414    materials: &[RawMaterial],
1415    temperature: f32,
1416    dt: i64,
1417    counters: &mut crate::work::tracker::StockCounters,
1418) {
1419    home.fuel -= dt;
1420    let storage = &mut storages[home.storage];
1421    if 0 < stored(storage, materials, 5) {
1422        crate::people::status::set_icon(&mut home.status, 6, false, false);
1423    }
1424    if 0 < stored(storage, materials, 6) {
1425        crate::people::status::set_icon(&mut home.status, 7, false, false);
1426    }
1427    if temperature < home.desc.warm_temperature && home.fuel < 0 && !home.families.is_empty() {
1428        let fuel = storage
1429            .items
1430            .iter()
1431            .find(|i| materials[i.material].flags >> 6 & 1 != 0 && 0 < i.count - i.reserved)
1432            .map(|i| i.material);
1433        if let Some(m) = fuel {
1434            home.fuel = ticks(home.desc.fuel_seconds);
1435            storage.remove(m, 1);
1436            if let Some(slot) = crate::work::produce::production_group(materials[m].flags) {
1437                counters.used[slot] += 1;
1438            }
1439        }
1440    }
1441}