Skip to main content

banished_game/production/
forester.rs

1//! The forester's lodge and its work. See docs/JOBS.md, "Foresters".
2//!
3//! Like the gatherer, a worker first takes the building's own piles (AutoPickup), then draws random
4//! cells within the radius. An empty cell in the entrance's region plants a sapling (while planting
5//! is on); a grown, unclaimed natural resource whose raw material is neither food nor herbs is cut
6//! (while cutting is on and the logs are under their limit).
7// vtable 0x140169890; vtable 0x1401677c0; +0x90
8
9use crate::behaviour::character::{Action, MoveTo};
10use crate::nature::seeding::Seeder;
11use crate::pathfind::{nearest_neighbour, WALKABLE};
12use crate::production::herbalist::HerbalistWork;
13use crate::scheduler::ticks;
14use crate::town::Sim;
15use crate::work::jobs::{Work, WorkKind};
16
17/// A resolved ForesterDescription.
18#[derive(Debug, Clone, Default, PartialEq)]
19pub struct ForesterTemplate {
20    // +0x20
21    /// The tree it plants (index into the placed-object templates).
22    pub tree: usize,
23    // +0x28
24    /// The stock-manager slot whose limit stops the cutting.
25    pub group: i32,
26}
27
28// +0x50; +0x51
29/// ForesterComponent plant and cut (the lodge's two toggle buttons).
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub struct Forester {
32    /// Whether it plants trees.
33    pub plant: bool,
34    /// Whether it cuts trees.
35    pub cut: bool,
36}
37
38impl Sim<'_, '_> {
39    /// Slot `+0x90` (`FUN_140124c50`): outside the idle state (0xe), a lodge with both
40    /// toggles off gets both on; then the work.
41    pub(crate) fn forester_work(&mut self, e: usize) -> Work {
42        if let Some(site) = self.sites.get_mut(&e) {
43            let m = &site.machine;
44            let state = if m.pending < 0 { m.state } else { m.pending };
45            if let Some(f) = &mut site.forester {
46                if state != 0xe && !f.plant && !f.cut {
47                    *f = Forester {
48                        plant: true,
49                        cut: true,
50                    };
51                }
52            }
53        }
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::Forester(HerbalistWork::default()),
65        )
66    }
67
68    /// Slot `+0x20` (`FUN_1400f0390`) after the AutoPickup offers.
69    pub(crate) fn forester_try(&mut self, work: &mut Work, c: usize) -> u8 {
70        let e = work.entity;
71        let Some(site) = self.sites.get(&e) else {
72            return 0;
73        };
74        let toggles = site.forester.unwrap_or_default();
75        let t = &self.site_templates[site.template];
76        let Some(desc) = t.forester.clone() else {
77            return 0;
78        };
79        let radius = t.radius.unwrap_or(0);
80        let group = desc.group.clamp(0, 20) as usize;
81        let (count, limit) = self.stock[group];
82        self.set_icon(e, crate::work::professions::icon::LIMIT, limit <= count);
83        let WorkKind::Forester(h) = &work.kind else {
84            return 0;
85        };
86        if self.work_max(e) < h.workers.len() as i32 {
87            return 1;
88        }
89        let n = self.size as i32;
90        let r2 = radius * radius;
91        let attempts = (r2 as f32 * std::f32::consts::PI).sqrt() as i32;
92        let (cx, cy) = (work.cell % n, work.cell / n);
93        let from = self.citizen_cell(c);
94        let entrance = self.entrance_cell(e);
95        let in_map = |cell: i32| 0 <= cell && cell < n * n;
96        for _ in 0..attempts {
97            let (x, y) = loop {
98                let (dx, dy) = loop {
99                    let dx = self.rng.rand_tol(radius);
100                    let dy = self.rng.rand_tol(radius);
101                    if dx * dx + dy * dy <= r2 {
102                        break (dx, dy);
103                    }
104                };
105                // As in the gatherer's, only the row is kept on the map.
106                let (x, y) = (cx + dx, cy + dy);
107                if 0 <= y && y < n {
108                    break (x, y);
109                }
110            };
111            let cell = n * y + x;
112            // Deviation: cells off the map's edge (and an entrance off the map) read outside
113            // the cell array in the original; no candidate here.
114            if !in_map(cell) || !in_map(entrance) {
115                continue;
116            }
117            let (ci, ei) = (cell as usize, entrance as usize);
118            let bit = |ty: u8| 1u32 << (ty & 0x1f);
119            let (cb, eb) = (bit(self.cells.types[ci].0), bit(self.cells.types[ei].0));
120            if cb & 0x1e0 != 0 {
121                continue;
122            }
123            // `FUN_1400a1950` relabels the regions first; the port keeps them current.
124            let (rc, re) = (self.regions[ci], self.regions[ei]);
125            let joined = (cb | eb) & 0x1e0 != 0 || (rc != 0 && re != 0 && rc == re);
126            if !joined || self.cells.occupants[ci].is_some() {
127                continue;
128            }
129            if self.geometry_rank(entrance, cell) > radius * 2 {
130                continue;
131            }
132            let object = self.cells.objects[ci];
133            if toggles.plant && object.is_none() {
134                if !self.reachable(cell, from) {
135                    continue;
136                }
137                let admits = crate::world::placement::cell_admits(
138                    self.cells,
139                    self.terrain,
140                    x,
141                    y,
142                    4,
143                    0.97,
144                    1000.0,
145                    0.0,
146                    false,
147                    false,
148                    |_| false,
149                );
150                if !admits || !self.seeder().density_ok(desc.tree, x, y) {
151                    continue;
152                }
153                let point = self.ground_at(cell);
154                self.plant_sequence(c, work, desc.tree, point, cell, None, None, 0);
155                return self.forester_took(work, c);
156            }
157            let Some(o) = object else { continue };
158            if !toggles.cut {
159                continue;
160            }
161            let template = self.placed[o].template;
162            if self.templates[template].natural.is_none() {
163                continue;
164            }
165            let (count, limit) = self.stock[group];
166            if count >= limit {
167                continue;
168            }
169            let flags = self.templates[template]
170                .pile
171                .and_then(|p| self.templates[p].pile_material)
172                .map_or(0, |m| self.materials[m].flags);
173            if flags & 0x20 != 0 || flags & 0x2000 != 0 {
174                continue;
175            }
176            if self.placed[o].growth < 1.0 || self.claimed.contains(&o) {
177                continue;
178            }
179            let Some(nb) = nearest_neighbour(self.size, &self.cells.types, cell, from, 0xe07)
180            else {
181                continue;
182            };
183            if !self.reachable(nb, from) {
184                continue;
185            }
186            let point = self.ground_at(nb);
187            self.removal_sequence(c, work, o, point, true);
188            return self.forester_took(work, c);
189        }
190        let misses = match &work.kind {
191            WorkKind::Forester(h) => h.misses,
192            _ => 0,
193        };
194        if attempts < misses {
195            if self.states[c].carried.used(self.materials) < 1 {
196                return 0;
197            }
198            self.drop_off(c);
199            return 2;
200        }
201        self.wait_sequence(c, work, 0);
202        self.took_job(c, work);
203        if let WorkKind::Forester(h) = &mut work.kind {
204            h.workers.insert(c);
205            h.misses += 1;
206        }
207        2
208    }
209
210    fn forester_took(&mut self, work: &mut Work, c: usize) -> u8 {
211        self.took_job(c, work);
212        if let WorkKind::Forester(h) = &mut work.kind {
213            h.misses = 0;
214            h.workers.insert(c);
215        }
216        2
217    }
218
219    /// `FUN_1400c3bb0`: take the tool out, walk to the cell, work 2.1 s, plant (the cell must
220    /// have `occupant`), put the tool away, send `message`, report `param`. The forester has no
221    /// occupant or message; an orchard has itself and its message 0.
222    #[allow(clippy::too_many_arguments)]
223    pub(crate) fn plant_sequence(
224        &mut self,
225        c: usize,
226        work: &mut Work,
227        tree: usize,
228        point: [f32; 3],
229        cell: i32,
230        occupant: Option<usize>,
231        message: Option<crate::behaviour::character::Message>,
232        param: i32,
233    ) {
234        self.clear_citizen(c, Some(work));
235        self.on_empty_think(c);
236        self.push(
237            c,
238            Action::CompleteTask {
239                work: work.id,
240                param,
241            },
242        );
243        if let Some(m) = message {
244            self.push(c, Action::SendMessage(m));
245        }
246        self.push(c, Action::ShowTool(-1));
247        self.push(
248            c,
249            Action::CreateNaturalResource {
250                template: tree,
251                occupant,
252                cell,
253            },
254        );
255        self.push(c, self.perform(c, -1000.0, 0xc, 2.1, true));
256        let walk = MoveTo::new(point, 3, 0, WALKABLE, 0, -1.0, self.rng);
257        self.push(c, Action::move_to(walk));
258        self.push(c, Action::ShowTool(3));
259    }
260
261    /// `FUN_1400c7960(citizen, work, param)`: stand 0.25 s, then report `param`.
262    pub(crate) fn wait_sequence(&mut self, c: usize, work: &mut Work, param: i32) {
263        self.clear_citizen(c, Some(work));
264        self.on_empty_think(c);
265        self.push(
266            c,
267            Action::CompleteTask {
268                work: work.id,
269                param,
270            },
271        );
272        self.push(
273            c,
274            Action::PlayAnimation {
275                anim: 0,
276                duration: ticks(0.25),
277                elapsed: 0,
278            },
279        );
280    }
281
282    /// The seeding view of the town's map, for planting.
283    pub(crate) fn seeder(&mut self) -> Seeder<'_> {
284        self.removed.clear();
285        Seeder {
286            templates: self.templates,
287            terrain: self.terrain,
288            cells: self.cells,
289            rng: self.rng,
290            placed: self.placed,
291            removed: &mut self.removed,
292            animation_events: &mut self.animation_events,
293        }
294    }
295
296    /// CreateNaturalResource's start once the cell was found free.
297    pub fn plant(&mut self, template: usize, cell: i32) {
298        self.seeder().plant(template, cell);
299        *self.models_dirty = true;
300    }
301}