Skip to main content

banished_game/production/
herbalist.rs

1//! The gatherer's shelter and the herbalist, their work, and the workplace's auto-pickup. See
2//! docs/JOBS.md, "Gatherers".
3//!
4//! A worker first takes one of the piles its own building made (AutoPickup). Otherwise, while the
5//! stock is under the production group's limit, it draws random cells within the building's radius
6//! until one holds an unclaimed, fully grown natural resource of a listed kind, and harvests it.
7//! The pile it leaves is recorded for the AutoPickup. Too many unlucky draws in a row and the
8//! worker drops off what it carries instead of waiting.
9//!
10//! Original: `HerbalistComponent`; its work is made by slot `FUN_14004bac0`.
11// vtable 0x14015f980; vtable 0x1401679c0; +0x90
12
13use std::collections::BTreeSet;
14
15use crate::pathfind::nearest_neighbour;
16use crate::town::Sim;
17use crate::work::jobs::{Work, WorkKind};
18
19// +0x48; +0x50; +0x58
20/// The work's state: the workers (a set; its size), misses in a row.
21#[derive(Debug, Clone, Default, PartialEq)]
22pub struct HerbalistWork {
23    /// The workers.
24    pub workers: BTreeSet<usize>,
25    /// Misses in a row.
26    pub misses: i32,
27}
28
29// +0x28; +0x20; +0x30
30/// AutoPickupComponent (on, from AutoPickupDescription; the Pickup works of the piles the
31/// workplace's workers made).
32#[derive(Debug, Clone, Default, PartialEq)]
33pub struct AutoPickup {
34    /// Whether it is on.
35    pub enabled: bool,
36    /// The pickup works of the piles the workplace's workers made.
37    pub works: Vec<i32>,
38}
39
40/// A resolved HerbalistDescription.
41#[derive(Debug, Clone, Default, PartialEq)]
42pub struct HerbalistTemplate {
43    // +0x20
44    /// The natural-resource templates (indices into the placed-object templates).
45    pub resources: Vec<usize>,
46    // +0x30
47    /// The stock-manager slot whose limit stops the work.
48    pub group: i32,
49}
50
51impl Sim<'_, '_> {
52    /// Slot `+0x90` (`FUN_14004bac0`).
53    pub(crate) fn herbalist_work(&self, e: usize) -> Work {
54        let profession = self
55            .workforce
56            .workplaces
57            .get(&e)
58            .and_then(|w| w.profession)
59            .unwrap_or(self.professions.laborer);
60        Work::new(
61            e,
62            self.rect_cell(e),
63            profession,
64            WorkKind::Herbalist(HerbalistWork::default()),
65        )
66    }
67
68    /// `FUN_1400e9280`: queue the AutoPickup's works that still exist, in order, to be offered
69    /// (`Jobs::OfferOne` with a second try); the others leave the list. False when there is
70    /// nothing to offer.
71    pub(crate) fn auto_pickup_offers(&mut self, e: usize, exists: impl Fn(i32) -> bool) -> bool {
72        let Some(a) = self.sites.get_mut(&e).and_then(|s| s.auto_pickup.as_mut()) else {
73            return false;
74        };
75        if !a.enabled {
76            return false;
77        }
78        a.works.retain(|&id| exists(id));
79        if a.works.is_empty() {
80            return false;
81        }
82        self.offers = a.works.clone();
83        self.offers_again = true;
84        true
85    }
86
87    /// `FUN_140045170`: a pile's Pickup work joins its workplace's AutoPickup list.
88    pub(crate) fn auto_pickup_add(&mut self, e: usize, work: i32) {
89        if let Some(a) = self.sites.get_mut(&e).and_then(|s| s.auto_pickup.as_mut()) {
90            if a.enabled {
91                a.works.push(work);
92            }
93        }
94    }
95
96    /// Slot `+0x20` (`FUN_1400f3880`) after the AutoPickup offers.
97    pub(crate) fn herbalist_try(&mut self, work: &mut Work, c: usize) -> u8 {
98        let e = work.entity;
99        let Some(site) = self.sites.get(&e) else {
100            return 0;
101        };
102        let t = &self.site_templates[site.template];
103        let Some(desc) = t.herbalist.clone() else {
104            return 0;
105        };
106        let radius = t.radius.unwrap_or(0);
107        // The production group's limit (also the status icon 2).
108        let (count, limit) = self.stock[desc.group.clamp(0, 20) as usize];
109        let limited = limit <= count;
110        self.set_icon(e, crate::work::professions::icon::LIMIT, limited);
111        let WorkKind::Herbalist(h) = &work.kind else {
112            return 0;
113        };
114        if self.work_max(e) < h.workers.len() as i32 || limited {
115            return 1;
116        }
117        let n = self.size as i32;
118        let r2 = radius * radius;
119        let attempts = (r2 as f32 * std::f32::consts::PI).sqrt() as i32;
120        let (cx, cy) = (work.cell % n, work.cell / n);
121        let from = self.citizen_cell(c);
122        let entrance = self.entrance_cell(e);
123        let mut hit = None;
124        'draws: for _ in 0..attempts {
125            let (x, y) = loop {
126                let (dx, dy) = loop {
127                    let dx = self.rng.rand_tol(radius);
128                    let dy = self.rng.rand_tol(radius);
129                    if dx * dx + dy * dy <= r2 {
130                        break (dx, dy);
131                    }
132                };
133                // The original's column test (`x < 0 && n <= x`) can never hold: only the
134                // row is kept on the map.
135                let (x, y) = (cx + dx, cy + dy);
136                if 0 <= y && y < n {
137                    break (x, y);
138                }
139            };
140            let cell = n * y + x;
141            // Deviation: a column off the map's edge reads past the cell array in the
142            // original; such a cell is no candidate here.
143            if cell < 0 || cell >= n * n {
144                continue;
145            }
146            let ty = self.cells.types[cell as usize].0;
147            if (1u32 << (ty & 0x1f)) & 0x1e0 != 0 {
148                continue;
149            }
150            if self.geometry_rank(entrance, cell) > radius * 2 {
151                continue;
152            }
153            let Some(o) = self.cells.objects[cell as usize] else {
154                continue;
155            };
156            let p = &self.placed[o];
157            if self.templates[p.template].natural.is_none() {
158                continue;
159            }
160            for &kind in &desc.resources {
161                if p.growth < 1.0 || p.template != kind || self.claimed.contains(&o) {
162                    continue;
163                }
164                let Some(nb) = nearest_neighbour(self.size, &self.cells.types, cell, from, 0xe07)
165                else {
166                    continue;
167                };
168                if !self.reachable(nb, from) {
169                    continue;
170                }
171                hit = Some((o, nb));
172                break 'draws;
173            }
174        }
175        if let Some((o, nb)) = hit {
176            let point = self.ground_at(nb);
177            self.removal_sequence(c, work, o, point, true);
178            self.took_job(c, work);
179            if let WorkKind::Herbalist(h) = &mut work.kind {
180                h.misses = 0;
181                h.workers.insert(c);
182            }
183            return 2;
184        }
185        let misses = match &work.kind {
186            WorkKind::Herbalist(h) => h.misses,
187            _ => 0,
188        };
189        if attempts < misses {
190            // `FUN_1401323e0` of the carried storage, then `Activity::DropOff`.
191            if self.states[c].carried.used(self.materials) < 1 {
192                return 0;
193            }
194            self.drop_off(c);
195            return 2;
196        }
197        self.wait_sequence(c, work, 0);
198        self.took_job(c, work);
199        if let WorkKind::Herbalist(h) = &mut work.kind {
200            h.workers.insert(c);
201            h.misses += 1;
202        }
203        2
204    }
205
206    /// `FUN_14008d480(entity, 0)` on the town view.
207    pub(crate) fn entrance_cell(&mut self, e: usize) -> i32 {
208        let town = crate::behaviour::activities::TownView {
209            size: self.size,
210            types: &self.cells.types,
211            regions: self.regions,
212            occupants: &self.cells.occupants,
213            objects: &self.cells.objects,
214            floors: &self.cells.floors,
215            entities: self.entities,
216            happiness: self.happiness,
217        };
218        town.entrance(e, self.rng)
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    #[test]
225    fn attempts_follow_the_disc_area() {
226        // `(int)sqrt(r² · π)` for the gatherer's radius of 20 cells.
227        let r2 = 20 * 20;
228        assert_eq!((r2 as f32 * std::f32::consts::PI).sqrt() as i32, 35);
229    }
230}