Skip to main content

banished_game/people/
services.rs

1//! Services: AttendanceComponent (schools, the chapel, the doctor), HealerComponent (the
2//! herbalist), and `LocationWork`, the work that keeps their staff at the building. See
3//! docs/CITIZEN-LIFE.md.
4// vtable 0x140167ab0
5
6use crate::behaviour::character::{Action, MoveTo};
7use crate::pathfind::WALKABLE;
8use crate::scheduler::ticks;
9use crate::town::Sim;
10use crate::work::construction::Attendance;
11use crate::work::jobs::{Jobs, Work, WorkKind};
12use crate::work::stock::StorageSearchMap;
13
14/// `LocationWork`: one staff member at a time stands at the building.
15#[derive(Debug, Clone, Default, PartialEq)]
16pub struct LocationWork {
17    // +0x48
18    /// Not identified yet.
19    pub worker: Option<usize>,
20}
21
22impl Sim<'_, '_> {
23    /// `FUN_1400445f0`: register (`+0x60`), set a LocationWork, enable the workplace.
24    pub(crate) fn attendance_enter(&mut self, jobs: &mut Jobs, e: usize) {
25        let site = &self.sites[&e];
26        let capacity = self.site_templates[site.template].attendance.unwrap_or(15);
27        let profession = self.workforce.workplaces.get(&e).and_then(|w| w.profession);
28        self.attendance.entry(e).or_insert(Attendance {
29            capacity,
30            attendees: Vec::new(),
31            profession,
32        });
33        if !self.attendance_list.contains(&e) {
34            self.attendance_list.push(e);
35        }
36        let work = Work::new(
37            e,
38            self.rect_cell(e),
39            profession.unwrap_or(self.professions.laborer),
40            WorkKind::Location(LocationWork::default()),
41        );
42        self.set_work(jobs, e, work, 1, false);
43        if let Some(w) = self.workforce.workplaces.get_mut(&e) {
44            w.enabled = true;
45        }
46    }
47
48    /// `FUN_140044530`: unregister, send everyone away, disable the workplace.
49    pub(crate) fn attendance_leave(&mut self, e: usize) {
50        self.attendance_list.retain(|&x| x != e);
51        self.evict(e);
52        self.enable_workplace(e, false);
53    }
54
55    /// `FUN_1400448c0`: every attendee leaves (school, doctor or chapel).
56    pub(crate) fn evict(&mut self, e: usize) {
57        let attendees = self
58            .attendance
59            .get_mut(&e)
60            .map(|a| std::mem::take(&mut a.attendees))
61            .unwrap_or_default();
62        for c in attendees {
63            let s = &mut self.states[c];
64            if s.school == Some(e) {
65                s.school = None;
66            }
67            if s.doctor == Some(e) {
68                s.doctor = None;
69            }
70            if s.priest == Some(e) {
71                s.priest = None;
72            }
73        }
74    }
75
76    /// The scheduled update of a school, chapel or doctor: when nobody works there, everyone
77    /// leaves.
78    ///
79    /// Original: `FUN_140044cd0`.
80    pub fn update_attendance(&mut self, visits: &[(usize, i64)]) {
81        let empty: Vec<usize> = visits
82            .iter()
83            .filter_map(|&(e, _)| Some((e, self.attendance.get(&e)?)))
84            .filter(|(e, a)| {
85                !a.attendees.is_empty()
86                    && self
87                        .workforce
88                        .workplaces
89                        .get(e)
90                        .is_none_or(|w| w.workers.is_empty())
91            })
92            .map(|(e, _)| e)
93            .collect();
94        for e in empty {
95            self.evict(e);
96        }
97    }
98
99    /// `FUN_140044710` for the status icon 2 at `FUN_140044590`: every registered attendance
100    /// with the same profession is full.
101    pub(crate) fn attendance_full(&self, e: usize) -> bool {
102        let profession = |x: &usize| self.workforce.workplaces.get(x).and_then(|w| w.profession);
103        !self.attendance_list.iter().any(|x| {
104            profession(x) == profession(&e)
105                && self
106                    .attendance
107                    .get(x)
108                    .is_some_and(|a| (a.attendees.len() as i32) < a.capacity)
109        })
110    }
111
112    /// LocationWork slot `+0x20` (`FUN_1400f4d50`).
113    pub(crate) fn location_try(&mut self, work: &mut Work, c: usize) -> u8 {
114        let WorkKind::Location(l) = &work.kind else {
115            return 0;
116        };
117        if l.worker.is_some() {
118            return 1;
119        }
120        if self.states[c].carried.used(self.materials) > 0 {
121            self.drop_off(c);
122        }
123        // `FUN_1401280b0`: the nearest reachable use point (category 0).
124        let from = self.citizen_cell(c);
125        let Some(target) = self.nearest_use_point(work.entity, from) else {
126            return 0;
127        };
128        // `FUN_1400c0960` with a CompleteTask.
129        self.clear_citizen(c, Some(work));
130        self.on_empty_think(c);
131        self.push(
132            c,
133            Action::CompleteTask {
134                work: work.id,
135                param: 0,
136            },
137        );
138        self.push(
139            c,
140            Action::PerformWork {
141                facing: -1000.0,
142                anim: 8,
143                duration: ticks(1.0),
144                elapsed: 0,
145                consume: false,
146            },
147        );
148        let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
149        self.push(c, Action::move_to(walk));
150        let s = &mut self.states[c];
151        s.health_events |= 4;
152        s.job = work.id;
153        s.supply_activity = 0;
154        if work.profession == s.profession {
155            s.flags |= 0x20000;
156        }
157        if let WorkKind::Location(l) = &mut work.kind {
158            l.worker = Some(c);
159        }
160        2
161    }
162
163    /// `FUN_1401280b0` over an entity's use points: the one with the lowest path rank from
164    /// `from`.
165    pub(crate) fn nearest_use_point(&mut self, e: usize, from: i32) -> Option<[f32; 3]> {
166        let points = self.entities.get(e)?.use_points.clone();
167        let mut best = 0xffff;
168        let mut found = None;
169        for p in points {
170            let to = self.cell_of(p);
171            let town = crate::behaviour::activities::TownView {
172                size: self.size,
173                types: &self.cells.types,
174                regions: self.regions,
175                occupants: &self.cells.occupants,
176                objects: &self.cells.objects,
177                floors: &self.cells.floors,
178                entities: self.entities,
179                happiness: self.happiness,
180            };
181            if !town.reachable(WALKABLE, from, to) {
182                continue;
183            }
184            let rank = self.geometry_rank(from, to);
185            if rank < best {
186                best = rank;
187                found = Some(p);
188            }
189        }
190        found
191    }
192
193    /// The path-rank distance (`FUN_1400a4ce0`, else map width plus the straight distance).
194    pub(crate) fn geometry_rank(&mut self, a: i32, b: i32) -> i32 {
195        let (size, terrain) = (self.size, self.terrain);
196        let floors = &self.cells.floors;
197        let ground = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
198        let town = crate::behaviour::activities::TownView {
199            size: self.size,
200            types: &self.cells.types,
201            regions: self.regions,
202            occupants: &self.cells.occupants,
203            objects: &self.cells.objects,
204            floors: &self.cells.floors,
205            entities: self.entities,
206            happiness: self.happiness,
207        };
208        let mut g = crate::behaviour::activities::StorageGeometry {
209            town: &town,
210            sectors: self.sectors,
211            ground: &ground,
212        };
213        g.rank(a, b)
214    }
215}