Skip to main content

banished_game/production/
fishing.rs

1//! FishingComponent (the fisherman's dock) and `FishingWork`. See docs/JOBS.md "Fishermen".
2//!
3//! A registered dock counts on map layer 8 within its radius, so docks whose waters overlap share
4//! the fish. A worker takes a free use point and a free create point, draws up to ten cells within
5//! the radius for water of the description's kinds, and fishes from the use point for 30 s. The
6//! catch is the fish's yield times the dock's water share (`1 / docks there · water fraction`),
7//! left on the create point.
8//!
9//! Original: `FUN_1400eff90`.
10// vtable 0x140169660; vtable 0x140167740
11
12use crate::behaviour::character::{Action, MoveTo};
13use crate::pathfind::WALKABLE;
14use crate::town::Sim;
15use crate::work::jobs::{Work, WorkKind};
16
17/// A resolved FishingDescription (versioned).
18///
19/// Original: `FUN_140124640`.
20#[derive(Debug, Clone, Default, PartialEq)]
21pub struct FishingTemplate {
22    // +0x20
23    /// The catch's raw-material template (a pile template).
24    pub output: usize,
25    // +0x2c
26    /// Cell types counted as fishing water.
27    pub mask: u32,
28    // +0x28
29    /// (version 1): the stock-manager slot whose limit stops the work.
30    pub group: i32,
31}
32
33// +0x50; +0x54
34/// FishingComponent registered on the map, the water fraction.
35#[derive(Debug, Clone, Copy, Default, PartialEq)]
36pub struct Fishing {
37    /// Whether it is registered on the map.
38    pub registered: bool,
39    /// The share of water around it.
40    pub water: f32,
41}
42
43// +0x48; +0x58; +0x68
44/// `FishingWork`: the worker at each use point, each create point's state (0 free, 1 a catch is
45/// coming, 2 holds a catch), misses in a row.
46#[derive(Debug, Clone, Default, PartialEq)]
47pub struct FishingWork {
48    /// The worker at each use point.
49    pub workers: Vec<Option<usize>>,
50    /// Each create point's state: 0 free, 1 a catch is coming, 2 holds a catch.
51    pub points: Vec<i32>,
52    /// Misses in a row.
53    pub misses: i32,
54}
55
56/// The fishing layer of [`crate::nature::seeding::Cells::layers`].
57pub const LAYER: usize = 8;
58
59impl Sim<'_, '_> {
60    fn fishing_template(&self, e: usize) -> Option<(FishingTemplate, i32)> {
61        let t = &self.site_templates[self.sites.get(&e)?.template];
62        Some((t.fishing.clone()?, t.radius.unwrap_or(0)))
63    }
64
65    /// The map rectangle's centre cell `(x + w / 2, y + h / 2)`.
66    fn rect_centre(&self, e: usize) -> (i32, i32) {
67        let (x, y) = self.placed[e].cell;
68        let (w, h) = self.placed[e].footprint;
69        ((w >> 1) + x, (h >> 1) + y)
70    }
71
72    /// Slot `+0x90` (`FUN_1401244b0`) → `FUN_1400eff90`.
73    pub(crate) fn fishing_work(&self, e: usize) -> Work {
74        let t = &self.templates[self.placed[e].template];
75        let profession = self
76            .workforce
77            .workplaces
78            .get(&e)
79            .and_then(|w| w.profession)
80            .unwrap_or(self.professions.laborer);
81        Work::new(
82            e,
83            self.rect_cell(e),
84            profession,
85            WorkKind::Fishing(FishingWork {
86                workers: vec![None; t.use_points.len()],
87                points: vec![0; t.create_points.len()],
88                misses: 0,
89            }),
90        )
91    }
92
93    /// Slot `+0x98` (`FUN_140124350`, after state 0xd's enter): count on layer 8
94    /// (`FUN_1401243a0(1)`), then the water fraction.
95    pub(crate) fn fishing_register(&mut self, e: usize) {
96        let Some(f) = self.sites.get(&e).and_then(|s| s.fishing) else {
97            return;
98        };
99        if f.registered {
100            return;
101        }
102        self.fishing_layer(e, 1);
103        self.fishing_water(e);
104        if let Some(f) = self.sites.get_mut(&e).and_then(|s| s.fishing.as_mut()) {
105            f.registered = true;
106        }
107    }
108
109    /// Slot `+0xa0` (`FUN_140124320`: state 0xe's enter and the 0xd → 3 transition).
110    pub(crate) fn fishing_unregister(&mut self, e: usize) {
111        if !self
112            .sites
113            .get(&e)
114            .and_then(|s| s.fishing)
115            .is_some_and(|f| f.registered)
116        {
117            return;
118        }
119        self.fishing_layer(e, -1);
120        if let Some(f) = self.sites.get_mut(&e).and_then(|s| s.fishing.as_mut()) {
121            f.registered = false;
122        }
123    }
124
125    /// `FUN_1401243a0`.
126    fn fishing_layer(&mut self, e: usize, delta: i32) {
127        let Some((_, radius)) = self.fishing_template(e) else {
128            return;
129        };
130        let (cx, cy) = self.rect_centre(e);
131        self.cells.add_layer(LAYER, cx, cy, radius, delta);
132    }
133
134    /// Slot `+0xa8` (`FUN_1401240e0`, also state 0xd's scheduled update): the fishing water
135    /// in the disc over half the disc's cells on the map.
136    pub(crate) fn fishing_water(&mut self, e: usize) {
137        let Some((d, r)) = self.fishing_template(e) else {
138            return;
139        };
140        let (cx, cy) = self.rect_centre(e);
141        let n = self.size as i32;
142        let (mut cells, mut water) = (0, 0);
143        for dx in -r..r {
144            for dy in -r..r {
145                let (x, y) = (cx + dx, cy + dy);
146                if dx * dx + dy * dy > r * r || x < 0 || y < 0 || x >= n || y >= n {
147                    continue;
148                }
149                cells += 1;
150                let ty = self.cells.types[(n * y + x) as usize].0;
151                if d.mask >> (ty & 0x1f) & 1 != 0 {
152                    water += 1;
153                }
154            }
155        }
156        if let Some(f) = self.sites.get_mut(&e).and_then(|s| s.fishing.as_mut()) {
157            f.water = water as f32 / (cells as f32 * 0.5);
158        }
159    }
160
161    /// Slot `+0x20` (`FUN_1400ef3f0`).
162    pub(crate) fn fishing_try(&mut self, work: &mut Work, c: usize) -> u8 {
163        let e = work.entity;
164        let Some((d, radius)) = self.fishing_template(e) else {
165            return 0;
166        };
167        let water = self.sites[&e].fishing.unwrap_or_default().water;
168        let (count, limit) = self.stock[d.group.clamp(0, 20) as usize];
169        self.set_icon(e, crate::work::professions::icon::LIMIT, limit <= count);
170        if limit <= count {
171            return 1;
172        }
173        let WorkKind::Fishing(fw) = &mut work.kind else {
174            return 0;
175        };
176        let Some(slot) = fw.workers.iter().position(Option::is_none) else {
177            return 1;
178        };
179        // Create points: a held catch that has gone is free again; take the first free one.
180        let mut free = None;
181        for i in 0..fw.points.len() {
182            let cell = self.create_cell(e, i);
183            if fw.points[i] == 2 && self.cells.objects[cell as usize].is_none() {
184                fw.points[i] = 0;
185            }
186            if free.is_none() && fw.points[i] == 0 {
187                free = Some((i, cell));
188            }
189        }
190        let Some((point, out_cell)) = free else {
191            // `Jobs::OfferOne(.., 1)` for the catches on the create points, in order.
192            let n = fw.points.len();
193            self.offers = (0..n)
194                .filter_map(|i| self.cells.objects[self.create_cell(e, i) as usize])
195                .filter_map(|o| self.piles.get(&o).map(|p| p.work))
196                .collect();
197            if self.offers.is_empty() {
198                return 1;
199            }
200            self.offers_again = true;
201            return crate::work::jobs::NESTED;
202        };
203        let (pos, yaw) = self.use_point(e, slot);
204        if !self.reachable(self.citizen_cell(c), self.cell_of(pos)) {
205            return 1;
206        }
207        let n = self.size as i32;
208        let r2 = radius * radius;
209        let (cx, cy) = (work.cell % n, work.cell / n);
210        for _ in 0..10 {
211            let (x, y) = loop {
212                let (dx, dy) = loop {
213                    let dx = self.rng.rand_tol(radius);
214                    let dy = self.rng.rand_tol(radius);
215                    if dx * dx + dy * dy <= r2 {
216                        break (dx, dy);
217                    }
218                };
219                // As in the gatherer's, only the row is kept on the map.
220                let (x, y) = (cx + dx, cy + dy);
221                if 0 <= y && y < n {
222                    break (x, y);
223                }
224            };
225            let cell = n * y + x;
226            // Deviation: a column off the map's edge reads outside the cell array in the
227            // original; no catch here.
228            if cell < 0 || cell >= n * n {
229                continue;
230            }
231            let i = cell as usize;
232            let docks = self.cells.layers[i][LAYER];
233            if d.mask >> (self.cells.types[i].0 & 0x1f) & 1 == 0 || docks == 0 {
234                continue;
235            }
236            let factor = 1.0 / docks as f32 * water;
237            let param = ((point as i32 | 0x800) << 4) | slot as i32;
238            self.fishing_sequence(c, work, pos, yaw, d.output, factor, out_cell, param);
239            self.took_job(c, work);
240            if let WorkKind::Fishing(fw) = &mut work.kind {
241                fw.points[point] = 1;
242                fw.workers[slot] = Some(c);
243                fw.misses = 0;
244            }
245            return 2;
246        }
247        let misses = match &work.kind {
248            WorkKind::Fishing(fw) => fw.misses,
249            _ => 0,
250        };
251        if misses < 0xb {
252            self.wait_sequence(c, work, slot as i32);
253            self.took_job(c, work);
254            if let WorkKind::Fishing(fw) = &mut work.kind {
255                fw.workers[slot] = Some(c);
256                fw.misses += 1;
257            }
258            return 2;
259        }
260        if self.states[c].carried.used(self.materials) < 1 {
261            return 1;
262        }
263        self.drop_off(c);
264        2
265    }
266
267    /// `FUN_1400c3fb0`: take the rod, walk to the use point, fish 30 s facing out, leave the
268    /// catch on the create point, message the FishingComponent (its default handler does
269    /// nothing; saves show the action), report, put the rod away.
270    #[allow(clippy::too_many_arguments)]
271    fn fishing_sequence(
272        &mut self,
273        c: usize,
274        work: &mut Work,
275        pos: [f32; 3],
276        yaw: f32,
277        output: usize,
278        factor: f32,
279        out_cell: i32,
280        param: i32,
281    ) {
282        self.clear_citizen(c, Some(work));
283        self.on_empty_think(c);
284        self.push(c, Action::ShowTool(-1));
285        self.push(
286            c,
287            Action::CompleteTask {
288                work: work.id,
289                param,
290            },
291        );
292        self.push(
293            c,
294            Action::SendMessage(crate::behaviour::character::Message {
295                component: crate::behaviour::character::Component::Fishing,
296                message: 0,
297                entity: Some(work.entity),
298                a: out_cell,
299                b: 0,
300            }),
301        );
302        let batch = self.states[c].carried.capacity;
303        self.push(
304            c,
305            Action::CreateRawMaterial {
306                template: output,
307                cell: out_cell,
308                batch,
309                workplace: Some(work.entity),
310                factor,
311            },
312        );
313        self.push(c, self.perform(c, yaw, 0xe, 30.0, true));
314        let walk = MoveTo::new(pos, 2, 0, WALKABLE, 0, -1.0, self.rng);
315        self.push(c, Action::move_to(walk));
316        self.push(c, Action::ShowTool(5));
317    }
318
319    /// Slot `+0x30` (`FUN_1400ef380`; the fail slot `FUN_1400ef350` calls it): the use point
320    /// is free; with bit 15 the create point holds a catch.
321    pub(crate) fn fishing_complete(work: &mut Work, param: i32) {
322        let WorkKind::Fishing(fw) = &mut work.kind else {
323            return;
324        };
325        if let Some(w) = fw.workers.get_mut((param & 0xf) as usize) {
326            *w = None;
327        }
328        if param >> 15 & 1 != 0 {
329            if let Some(p) = fw.points.get_mut(((param >> 4) & 0xf) as usize) {
330                *p = 2;
331            }
332        }
333    }
334
335    /// Slot `+0x48` (`FUN_1400ef100`): workers that lost the task free their use point;
336    /// then create points under way that no remaining worker holds hold a catch.
337    pub(crate) fn fishing_refresh(&mut self, work: &mut Work) {
338        let id = work.id;
339        let WorkKind::Fishing(fw) = &mut work.kind else {
340            return;
341        };
342        let mut lost = false;
343        let mut held = Vec::new();
344        for w in fw.workers.iter_mut() {
345            let Some(c) = *w else { continue };
346            match self.characters[c].stack.holds_task(id) {
347                None => {
348                    *w = None;
349                    lost = true;
350                }
351                Some(param) => held.push((param >> 4) & 0xf),
352            }
353        }
354        if lost {
355            for (i, p) in fw.points.iter_mut().enumerate() {
356                if !held.contains(&(i as i32)) && *p == 1 {
357                    *p = 2;
358                }
359            }
360        }
361    }
362}