Skip to main content

banished_game/people/
needs.rs

1//! The citizen activities that meet needs: eating (activities 0 and 14), warming up (1 and 15) and
2//! stocking the home with food and firewood (6 and 7). See docs/CITIZEN-LIFE.md.
3//!
4//! Original: `FUN_140112250`, `FUN_140112790`, `FUN_140111160`, `FUN_1401108b0`.
5
6use std::collections::BTreeMap;
7
8use banished_formats::types::AnimationGroup;
9
10use crate::behaviour::activities::{StorageGeometry, TownView};
11use crate::behaviour::agents::CitizenState;
12use crate::behaviour::ai::CitizenAi;
13use crate::behaviour::character::{work_facing, Action, Character, Component, Message, MoveTo};
14use crate::pathfind::WALKABLE;
15use crate::people::life::{stored, Home, LifeRules};
16use crate::random::Random;
17use crate::scheduler::ticks;
18use crate::sectors::SectorGraph;
19use crate::terrain::HeightGrid;
20use crate::work::stock::{find_storage, RawMaterial, Storage, StorageSite};
21
22/// `DAT_140178890`: an activity that found nothing waits this long (both its slots).
23const RETRY: f32 = 3.75;
24/// `DAT_1401788fc`, `DAT_140178934`: home-stocking cooldowns are `D[0xd8]` times these.
25const STOCK_UNREACHABLE: f32 = 75.0;
26const STOCK_NOTHING: f32 = 300.0;
27
28/// What the activities read and change besides the citizen.
29pub struct Needs<'a, 'b> {
30    /// A view of the town.
31    pub town: &'a TownView<'b>,
32    /// The sector graph.
33    pub sectors: &'a mut SectorGraph,
34    /// The storage sites.
35    pub sites: &'a [StorageSite],
36    /// The town's storages.
37    pub storages: &'a mut [Storage],
38    /// The raw materials.
39    pub materials: &'a [RawMaterial],
40    /// The homes, by entity.
41    pub homes: &'a mut BTreeMap<usize, Home>,
42    // +0x20
43    /// The town's residences, in the order they were completed: the searches over residences keep
44    /// the first of equal ranks.
45    ///
46    /// Original: `FUN_140093840`, `FUN_140094140`.
47    pub residences: &'a [usize],
48    /// The environment's temperature.
49    pub temperature: f32,
50    /// The citizen description's rules.
51    pub life: &'a LifeRules,
52    // +0x50
53    /// The laborer profession's carry.
54    pub carry: i32,
55    /// The ground's heights.
56    pub terrain: &'a HeightGrid<'b>,
57    /// The map's size in cells.
58    pub size: usize,
59    /// The game's random numbers.
60    pub rng: &'a mut Random,
61    // +0x70
62    /// The town's HealerComponents.
63    pub healers: &'a [usize],
64    /// Every WorkPlaceComponent, by entity.
65    pub workplaces: &'a BTreeMap<usize, crate::work::professions::WorkPlace>,
66    /// The AttendanceComponents, by entity.
67    pub attendance: &'a mut BTreeMap<usize, crate::work::construction::Attendance>,
68    /// Every citizen (play follows the guardian).
69    pub citizens: &'a [crate::people::citizens::Citizen],
70    /// Set when the school activity found no place to sit: the caller leaves the school and
71    /// looks for another.
72    ///
73    /// Original: `FUN_14011af20(c, 1)`.
74    pub reschool: bool,
75}
76
77/// The citizen an activity runs for.
78pub struct Who<'a> {
79    /// The citizen's AI.
80    pub ai: &'a mut CitizenAi,
81    /// The citizen's character.
82    pub ch: &'a mut Character,
83    /// The citizen's state.
84    pub state: &'a mut CitizenState,
85    /// The citizen's animation group.
86    pub group: &'a AnimationGroup,
87}
88
89impl Needs<'_, '_> {
90    /// `Interact::PointPosition(entity, 0)`: the first use point, else the rectangle centre.
91    fn point(&self, entity: usize) -> [f32; 3] {
92        let e = &self.town.entities[entity];
93        if let Some(p) = e.use_points.first() {
94            return *p;
95        }
96        crate::town::floor_ground(
97            self.town.floors,
98            self.size,
99            self.terrain,
100            self.town.point_cell(entity),
101        )
102    }
103
104    /// `FUN_1400c7e70`'s source: the model position, else the rectangle centre.
105    fn centre(&self, entity: usize) -> [f32; 3] {
106        let e = &self.town.entities[entity];
107        if e.has_model {
108            e.position
109        } else {
110            crate::town::floor_ground(
111                self.town.floors,
112                self.size,
113                self.terrain,
114                self.town.point_cell(entity),
115            )
116        }
117    }
118
119    fn home_food(&self, home: usize) -> i32 {
120        self.homes
121            .get(&home)
122            .map_or(0, |h| stored(&self.storages[h.storage], self.materials, 5))
123    }
124
125    fn warm_home(&self, entity: usize) -> bool {
126        self.homes
127            .get(&entity)
128            .is_some_and(|h| h.warm(self.temperature))
129    }
130
131    /// The storage of `entity` (a home's, or a registered town storage's).
132    fn storage(&self, entity: usize) -> Option<usize> {
133        self.homes.get(&entity).map(|h| h.storage).or_else(|| {
134            self.sites
135                .iter()
136                .find(|s| s.entity == entity)
137                .map(|s| s.storage)
138        })
139    }
140
141    /// `FUN_140093200` over the town's storages for category `category` (0 food, 1 fuel).
142    fn search(
143        &mut self,
144        category: u32,
145        cell: i32,
146        home: Option<usize>,
147    ) -> Option<(usize, [f32; 3])> {
148        let home_cell = home.map_or(-1, |h| self.town.entrance(h, self.rng));
149        let (size, terrain) = (self.size, self.terrain);
150        let floors = self.town.floors;
151        let ground = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
152        let mut geometry = StorageGeometry {
153            town: self.town,
154            sectors: self.sectors,
155            ground: &ground,
156        };
157        let found = find_storage(
158            self.sites,
159            self.storages,
160            self.materials,
161            category,
162            cell,
163            home_cell,
164            &mut geometry,
165            self.rng,
166        )?;
167        Some((self.sites[found.site].entity, found.target))
168    }
169
170    /// `FUN_140093840`: for the urgently hungry, the nearest of the home, the town's markets
171    /// and storages, and the empty residences that hold food, each taking a strictly better
172    /// rank in that order.
173    /// - The home: its entrance (`FUN_14008d480`, the ground at the cell's centre), when
174    ///   reachable, at its path rank (`FUN_140008830`), if it holds food (`FUN_140094410`).
175    /// - The markets, then the storages, as `FUN_140093200` weighs them but without its stop
176    ///   at a market covering the home.
177    /// - Each residence nobody lives in (`FUN_140017ac0`): its point 0, whose path rank
178    ///   (`FUN_1400a4ce0`, else the map width plus the straight distance) must beat the best;
179    ///   then the cells must be connected and the residence must hold food, and that rank is
180    ///   taken.
181    fn urgent_search(&mut self, cell: i32, home: Option<usize>) -> Option<(usize, [f32; 3])> {
182        let mut best = i32::MAX;
183        let mut found = None;
184        let (size, terrain) = (self.size, self.terrain);
185        let floors = self.town.floors;
186        let ground_at = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
187        if let Some(h) = home {
188            let entrance = self.town.entrance(h, self.rng);
189            if entrance >= 0 && self.town.reachable(WALKABLE, cell, entrance) {
190                let rank = self.rank(entrance, cell);
191                let food = self
192                    .homes
193                    .get(&h)
194                    .and_then(|r| self.storages[r.storage].find_item(self.materials, 0, self.rng));
195                if food.is_some() {
196                    best = rank;
197                    found = Some((h, ground_at(entrance)));
198                }
199            }
200        }
201        let (rank, storage) = {
202            let mut geometry = StorageGeometry {
203                town: self.town,
204                sectors: self.sectors,
205                ground: &ground_at,
206            };
207            crate::work::stock::scan_storages(
208                self.sites,
209                self.storages,
210                self.materials,
211                0,
212                cell,
213                None,
214                best,
215                &mut geometry,
216                self.rng,
217            )
218        };
219        if let Some(m) = storage {
220            best = rank;
221            found = Some((self.sites[m.site].entity, m.target));
222        }
223        let residences: Vec<usize> = self.residences.to_vec();
224        for e in residences {
225            let Some(home) = self.homes.get(&e) else {
226                continue;
227            };
228            if home.occupants() > 0 {
229                continue;
230            }
231            let p = self.point(e);
232            let to = self.town.cell_of(p);
233            let rank = self.rank(to, cell);
234            if best <= rank || !self.town.reachable(WALKABLE, cell, to) {
235                continue;
236            }
237            let storage = self.homes[&e].storage;
238            if self.storages[storage]
239                .find_item(self.materials, 0, self.rng)
240                .is_some()
241                && rank >= 0
242            {
243                best = rank;
244                found = Some((e, p));
245            }
246        }
247        found
248    }
249
250    /// The action stack emptied and set to call the AI back when done.
251    fn begin(&mut self, who: &mut Who) -> crate::behaviour::character::ActionStack {
252        let mut stack = std::mem::take(&mut who.ch.stack);
253        stack.clear(who.ch, who.group, self.storages);
254        stack.set_on_empty(vec![Action::SendMessage(Message {
255            component: Component::CitizenAi,
256            message: 0,
257            entity: None,
258            a: 0,
259            b: 0,
260        })]);
261        stack
262    }
263
264    /// Activities 0 and 14: eat, at home or from a storage.
265    ///
266    /// Original: `FUN_140112250`.
267    pub fn eat(&mut self, who: &mut Who) -> bool {
268        let cell = self.town.cell_of(who.ch.position);
269        let home = who.state.home;
270        let mut chosen = None;
271        if self.life.f(0xcc) < who.state.hunger {
272            // Urgent: food at home is eaten where the citizen stands.
273            if let Some(h) = home {
274                if 0 < self.home_food(h) {
275                    chosen = Some((h, who.ch.position, self.warm_home(h)));
276                }
277            }
278            if chosen.is_none() {
279                if let Some((e, p)) = self.urgent_search(cell, home) {
280                    let warm = self.warm_home(e);
281                    chosen = Some((e, p, warm));
282                }
283            }
284        }
285        if chosen.is_none() {
286            if let Some(h) = home {
287                if self.town.can_reach(cell, h, self.rng) && 0 < self.home_food(h) {
288                    chosen = Some((h, self.point(h), true));
289                }
290            }
291        }
292        if chosen.is_none() {
293            match self.search(0, cell, home) {
294                Some((e, p)) => {
295                    let warm = self.warm_home(e);
296                    chosen = Some((e, p, warm));
297                }
298                None => {
299                    who.ai.cooldowns[0] = ticks(RETRY);
300                    who.ai.cooldowns[14] = ticks(RETRY);
301                    return false;
302                }
303            }
304        }
305        let (entity, target, warm) = chosen.unwrap();
306        // `FUN_1400c28f0`.
307        let facing = work_facing(self.centre(entity), target);
308        let mut stack = self.begin(who);
309        if warm {
310            stack.push(who.ch, who.group, citizen_message(0, entity));
311        }
312        stack.push(who.ch, who.group, citizen_message(2, entity));
313        stack.push(
314            who.ch,
315            who.group,
316            Action::PerformWork {
317                facing,
318                anim: 8,
319                duration: ticks(2.0),
320                elapsed: 0,
321                consume: false,
322            },
323        );
324        let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
325        stack.push(who.ch, who.group, Action::move_to(walk));
326        who.ch.stack = stack;
327        who.state.job = -1;
328        who.state.health_events |= 4;
329        who.state.supply_activity = 0;
330        true
331    }
332
333    /// Activities 1 and 15: get warm, at the nearest warm residence when the citizen is very cold,
334    /// else at a warm home.
335    ///
336    /// Original: `FUN_140112790`.
337    pub fn warm(&mut self, who: &mut Who) -> bool {
338        let cell = self.town.cell_of(who.ch.position);
339        let mut chosen = None;
340        if self.life.f(0xec) < who.state.cold {
341            // `FUN_140094140`: path rank to each registered residence's point 0.
342            let (size, terrain) = (self.size, self.terrain);
343            let floors = self.town.floors;
344            let ground_at = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
345            let mut best = i32::MAX;
346            let homes: Vec<usize> = self
347                .residences
348                .iter()
349                .copied()
350                .filter(|e| self.homes.contains_key(e))
351                .collect();
352            for e in homes {
353                let p = self.point(e);
354                let to = self.town.cell_of(p);
355                let mut g = StorageGeometry {
356                    town: self.town,
357                    sectors: self.sectors,
358                    ground: &ground_at,
359                };
360                let rank = crate::work::stock::StorageSearchMap::rank(&mut g, to, cell);
361                if rank < best
362                    && self.town.reachable(WALKABLE, cell, to)
363                    && self.warm_home(e)
364                    && rank >= 0
365                {
366                    best = rank;
367                    chosen = Some((e, p));
368                }
369            }
370        }
371        if chosen.is_none() {
372            if let Some(h) = who.state.home.filter(|&h| self.warm_home(h)) {
373                chosen = Some((h, self.point(h)));
374            }
375        }
376        let Some((entity, target)) = chosen else {
377            who.ai.cooldowns[1] = ticks(RETRY);
378            who.ai.cooldowns[15] = ticks(RETRY);
379            return false;
380        };
381        // `FUN_1400c2c60`: no facing (−1000).
382        let mut stack = self.begin(who);
383        stack.push(who.ch, who.group, citizen_message(0, entity));
384        stack.push(
385            who.ch,
386            who.group,
387            Action::PerformWork {
388                facing: -1000.0,
389                anim: 8,
390                duration: ticks(4.0),
391                elapsed: 0,
392                consume: false,
393            },
394        );
395        let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
396        stack.push(who.ch, who.group, Action::move_to(walk));
397        who.ch.stack = stack;
398        who.state.job = -1;
399        who.state.health_events |= 4;
400        who.state.supply_activity = 0;
401        true
402    }
403
404    /// Activities 6 (food) and 7 (firewood, when `fuel`): carry food or firewood home from the
405    /// nearest storage. The caller first drops off anything carried (and queues this activity
406    /// again). `reachable` is the caller's test that the home can be reached.
407    ///
408    /// Original: `FUN_140111160` (food), `FUN_1401108b0` (firewood); the reach test is
409    /// `FUN_14011ada0`.
410    pub fn stock_home(&mut self, who: &mut Who, fuel: bool, reachable: bool) -> bool {
411        let (slot, category, icon, supply) = if fuel { (7, 1, 7, 2) } else { (6, 0, 6, 1) };
412        let cell = self.town.cell_of(who.ch.position);
413        let Some(home) = who.state.home.filter(|_| reachable) else {
414            who.ai.cooldowns[slot] = ticks(self.life.f(0xd8) * STOCK_UNREACHABLE);
415            return false;
416        };
417        let Some(home_storage) = self.storage(home) else {
418            return false;
419        };
420        let Some((source, from)) = self.search(category, cell, Some(home)) else {
421            who.ai.cooldowns[slot] = ticks(self.life.f(0xd8) * STOCK_NOTHING);
422            who.state.flags |= 4;
423            if stored(
424                &self.storages[home_storage],
425                self.materials,
426                if fuel { 6 } else { 5 },
427            ) == 0
428            {
429                if let Some(h) = self.homes.get_mut(&home) {
430                    crate::people::status::set_icon(&mut h.status, icon, true, false);
431                }
432            }
433            return false;
434        };
435        if let Some(h) = self.homes.get_mut(&home) {
436            crate::people::status::set_icon(&mut h.status, icon, false, false);
437        }
438        let Some(source_storage) = self.storage(source) else {
439            return false;
440        };
441        let target = self.point(home);
442        // As many units as the larger of the laborer's carry and the free carried capacity.
443        let free = who.state.carried.capacity - who.state.carried.used(self.materials);
444        let mut room = self.carry.max(free);
445        let mut items: Vec<(usize, i32)> = Vec::new();
446        while let Some(m) =
447            self.storages[source_storage].find_item(self.materials, category, self.rng)
448        {
449            let size = self.materials[m].size;
450            if room < size {
451                break;
452            }
453            self.storages[source_storage].reserve(m, 1);
454            match items.iter_mut().find(|(x, _)| *x == m) {
455                Some((_, n)) => *n += 1,
456                None => items.push((m, 1)),
457            }
458            room -= size;
459        }
460        // `FUN_1400c4c60` with no work: pick up, carry, drop everything, warm up at home.
461        let face_target = work_facing(self.centre(home), target);
462        let face_source = work_facing(self.centre(source), from);
463        let perform = |facing: f32, anim: usize| Action::PerformWork {
464            facing,
465            anim,
466            duration: ticks(0.75),
467            elapsed: 0,
468            consume: false,
469        };
470        let mut stack = self.begin(who);
471        stack.push(who.ch, who.group, citizen_message(0, home));
472        stack.push(who.ch, who.group, perform(face_target, 7));
473        stack.push(who.ch, who.group, Action::ShowTool(-1));
474        stack.push(
475            who.ch,
476            who.group,
477            Action::DropOff {
478                storage: home_storage,
479                material: usize::MAX,
480                count: -1,
481                // `FUN_1400c4c60`'s drop-off redirects.
482                redirect: true,
483            },
484        );
485        stack.push(who.ch, who.group, perform(face_target, 6));
486        let walk = MoveTo::new(target, 4, 0, WALKABLE, 0, -1.0, self.rng);
487        stack.push(who.ch, who.group, Action::move_to(walk));
488        stack.push(who.ch, who.group, perform(face_source, 7));
489        stack.push(who.ch, who.group, Action::ShowTool(-2));
490        stack.push(
491            who.ch,
492            who.group,
493            Action::PickupStoredRawMaterial {
494                storage: source_storage,
495                items,
496                partial: true,
497            },
498        );
499        stack.push(who.ch, who.group, perform(face_source, 6));
500        let walk = MoveTo::new(from, 1, 0, WALKABLE, 0, -1.0, self.rng);
501        stack.push(who.ch, who.group, Action::move_to(walk));
502        who.ch.stack = stack;
503        who.state.health_events |= 4;
504        who.state.job = -1;
505        who.state.supply_activity = supply;
506        true
507    }
508}
509
510impl Needs<'_, '_> {
511    /// The path-rank distance from `a` to `b`.
512    fn rank(&mut self, a: i32, b: i32) -> i32 {
513        let (size, terrain) = (self.size, self.terrain);
514        let floors = self.town.floors;
515        let ground_at = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
516        let mut g = StorageGeometry {
517            town: self.town,
518            sectors: self.sectors,
519            ground: &ground_at,
520        };
521        crate::work::stock::StorageSearchMap::rank(&mut g, a, b)
522    }
523
524    /// `FUN_1401280b0` over an entity's use points: the nearest reachable from `cell`.
525    fn nearest_point(&mut self, e: usize, cell: i32) -> Option<[f32; 3]> {
526        let points = self.town.entities.get(e)?.use_points.clone();
527        let mut best = 0xffff;
528        let mut found = None;
529        for p in points {
530            let to = self.town.cell_of(p);
531            if !self.town.reachable(WALKABLE, cell, to) {
532                continue;
533            }
534            let rank = self.rank(cell, to);
535            if rank < best {
536                best = rank;
537                found = Some(p);
538            }
539        }
540        found
541    }
542
543    /// `FUN_1400c0960(…, message, seconds, true)`: walk to `target`, work (no facing), then
544    /// Citizen message `message`.
545    fn visit(&mut self, who: &mut Who, target: [f32; 3], message: i32, secs: f32) {
546        let mut stack = self.begin(who);
547        stack.push(
548            who.ch,
549            who.group,
550            Action::SendMessage(Message {
551                component: Component::Citizen,
552                message,
553                entity: None,
554                a: 0,
555                b: 0,
556            }),
557        );
558        stack.push(
559            who.ch,
560            who.group,
561            Action::PerformWork {
562                facing: -1000.0,
563                anim: 8,
564                duration: ticks(secs),
565                elapsed: 0,
566                consume: false,
567            },
568        );
569        let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
570        stack.push(who.ch, who.group, Action::move_to(walk));
571        who.ch.stack = stack;
572        who.state.health_events |= 4;
573        who.state.job = -1;
574        who.state.supply_activity = 0;
575    }
576
577    /// `FUN_1400c2f20`: rest at home (walk there, warm up, 5 s).
578    fn rest_at_home(&mut self, who: &mut Who, home: usize) {
579        let target = self.point(home);
580        let mut stack = self.begin(who);
581        stack.push(
582            who.ch,
583            who.group,
584            Action::PerformWork {
585                facing: -1000.0,
586                anim: 8,
587                duration: ticks(5.0),
588                elapsed: 0,
589                consume: false,
590            },
591        );
592        stack.push(who.ch, who.group, citizen_message(0, home));
593        let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
594        stack.push(who.ch, who.group, Action::move_to(walk));
595        who.ch.stack = stack;
596        who.state.job = -1;
597        who.state.health_events |= 4;
598        who.state.supply_activity = 0;
599    }
600
601    /// Activity 10: sit a lesson at the school (message 4; its length is a value of the citizen
602    /// description).
603    ///
604    /// Original: `FUN_140112a30`.
605    pub fn school(&mut self, who: &mut Who) -> bool {
606        let Some(school) = who.state.school else {
607            return false;
608        };
609        let cell = self.town.cell_of(who.ch.position);
610        match self.nearest_point(school, cell) {
611            Some(target) => {
612                self.visit(who, target, 4, self.life.f(0x120));
613                true
614            }
615            None => {
616                self.reschool = true;
617                false
618            }
619        }
620    }
621
622    /// Activity 5: with a doctor, be treated (message 5); without one, or when the doctor cannot
623    /// be reached, rest at a home that can be reached. Like the original, it reports "not
624    /// started" after a treatment visit.
625    ///
626    /// Original: `FUN_140112c60`.
627    pub fn doctor(&mut self, who: &mut Who, me: usize) -> bool {
628        let cell = self.town.cell_of(who.ch.position);
629        let home = who
630            .state
631            .home
632            .filter(|&h| self.town.can_reach(cell, h, self.rng));
633        let Some(doctor) = who.state.doctor else {
634            return match home {
635                Some(h) => {
636                    self.rest_at_home(who, h);
637                    true
638                }
639                None => false,
640            };
641        };
642        match self.nearest_point(doctor, cell) {
643            Some(target) => {
644                self.visit(who, target, 5, self.life.f(0x124));
645                false
646            }
647            None => {
648                // `FUN_14011ae80`, then home.
649                who.state.doctor = None;
650                if let Some(a) = self.attendance.get_mut(&doctor) {
651                    a.attendees.retain(|&x| x != me);
652                }
653                match who.state.home {
654                    Some(h) => {
655                        self.rest_at_home(who, h);
656                        true
657                    }
658                    None => false,
659                }
660            }
661        }
662    }
663
664    /// Activity 4: take herbs from the nearest storage to the nearest herbalist with a worker, and
665    /// be treated there (5 seconds, message 3). The caller drops off what it carries first.
666    ///
667    /// Original: `FUN_140111d60`.
668    pub fn herbs(&mut self, who: &mut Who) -> bool {
669        who.ai.cooldowns[4] = ticks(75.0);
670        let cell = self.town.cell_of(who.ch.position);
671        let Some((source, from)) = self.search(3, cell, who.state.home) else {
672            return false;
673        };
674        let Some(storage) = self.storage(source) else {
675            return false;
676        };
677        let Some(herb) = self.storages[storage].find_item(self.materials, 3, self.rng) else {
678            return false;
679        };
680        // `FUN_140094700`: the nearest staffed, reachable healer.
681        let mut best = 0xffff;
682        let mut healer = None;
683        for &e in self.healers {
684            if self.workplaces.get(&e).is_none_or(|w| w.workers.is_empty()) {
685                continue;
686            }
687            let p = self.point(e);
688            let to = self.town.cell_of(p);
689            if !self.town.reachable(WALKABLE, cell, to) {
690                continue;
691            }
692            let rank = self.rank(cell, to);
693            if rank < best {
694                best = rank;
695                healer = Some((e, p));
696            }
697        }
698        let Some((healer, at)) = healer else {
699            return false;
700        };
701        // `FUN_1400c1c60`.
702        let face_healer = work_facing(self.centre(healer), at);
703        let face_source = work_facing(self.centre(source), from);
704        self.storages[storage].reserve(herb, 1);
705        let mut stack = self.begin(who);
706        stack.push(who.ch, who.group, citizen_message(3, healer));
707        stack.push(
708            who.ch,
709            who.group,
710            Action::ConsumeStoredRawMaterial {
711                storage: usize::MAX,
712                items: vec![(herb, 1)],
713                reserved: false,
714            },
715        );
716        stack.push(
717            who.ch,
718            who.group,
719            Action::PerformWork {
720                facing: face_healer,
721                anim: 8,
722                duration: ticks(5.0),
723                elapsed: 0,
724                consume: false,
725            },
726        );
727        let walk = MoveTo::new(at, 1, 0, WALKABLE, 0, -1.0, self.rng);
728        stack.push(who.ch, who.group, Action::move_to(walk));
729        stack.push(
730            who.ch,
731            who.group,
732            Action::PickupStoredRawMaterial {
733                storage,
734                items: vec![(herb, 1)],
735                partial: true,
736            },
737        );
738        stack.push(
739            who.ch,
740            who.group,
741            Action::PerformWork {
742                facing: face_source,
743                anim: 8,
744                duration: ticks(1.0),
745                elapsed: 0,
746                consume: false,
747            },
748        );
749        let walk = MoveTo::new(from, 1, 0, WALKABLE, 0, -1.0, self.rng);
750        stack.push(who.ch, who.group, Action::move_to(walk));
751        who.ch.stack = stack;
752        who.state.health_events |= 4;
753        who.state.job = -1;
754        who.state.supply_activity = 0;
755        who.ai.cooldowns[4] = ticks(self.life.f(0x78) * 75.0);
756        true
757    }
758
759    /// Activity 11: a child plays next to its guardian (or the guardian's partner), then waits
760    /// `RandFloat · 7 + 3` seconds.
761    ///
762    /// Original: `FUN_14010f780`.
763    pub fn play(&mut self, who: &mut Who, me: usize) -> bool {
764        let Some(mut g) = self.citizens.get(me).and_then(|c| c.parent) else {
765            return false;
766        };
767        if let Some(p) = self.citizens[g].spouse {
768            // `Rand(1)` is always 0: the partner.
769            if self.rng.rand_below(1) == 0 {
770                g = p;
771            }
772        }
773        let from = self.town.cell_of(self.citizens[g].position);
774        let to = self.town.cell_of(who.ch.position);
775        let Some(cell) =
776            crate::pathfind::nearest_neighbour(self.size, self.town.types, from, to, WALKABLE)
777        else {
778            return false;
779        };
780        let target = crate::town::floor_ground(self.town.floors, self.size, self.terrain, cell);
781        let idle = self.rng.rand_float() * 7.0 + 3.0;
782        // `FUN_1400c7b90`.
783        let mut stack = self.begin(who);
784        if 0.0 < idle {
785            stack.push(
786                who.ch,
787                who.group,
788                Action::PlayAnimation {
789                    anim: 0,
790                    duration: ticks(idle),
791                    elapsed: 0,
792                },
793            );
794        }
795        let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
796        stack.push(who.ch, who.group, Action::move_to(walk));
797        who.ch.stack = stack;
798        true
799    }
800}
801
802fn citizen_message(message: i32, entity: usize) -> Action {
803    Action::SendMessage(Message {
804        component: Component::Citizen,
805        message,
806        entity: Some(entity),
807        a: 0,
808        b: 0,
809    })
810}
811
812/// The home holds food for every living family (`D[0xc4]` per member of this citizen's family).
813///
814/// Original: `FUN_14011abb0`.
815pub fn home_has_food(
816    home: &Home,
817    slot: i32,
818    storages: &[Storage],
819    materials: &[RawMaterial],
820    life: &LifeRules,
821) -> bool {
822    let members = home.families.get(slot.max(0) as usize).map_or(0, Vec::len) as i32;
823    home.families_living() * members * life.f(0xc4) as i32
824        <= stored(&storages[home.storage], materials, 5)
825}
826
827/// `D[0xd4]` firewood per living family.
828///
829/// Original: `FUN_14011acc0`.
830pub fn home_has_fuel(
831    home: &Home,
832    storages: &[Storage],
833    materials: &[RawMaterial],
834    life: &LifeRules,
835) -> bool {
836    home.families_living() as f32 * life.f(0xd4)
837        <= stored(&storages[home.storage], materials, 6) as f32
838}