Skip to main content

banished_game/production/
farm.rs

1//! Crop fields: CropFieldComponent, and the works `PlantArea`, `TendArea` and the harvest work. See
2//! docs/FARMING.md.
3//!
4//! A field waits for spring (state 2), plants its crop on every cell (9), tends it while it grows
5//! (10) and harvests it when ripe or in autumn (11). The ClearArea controller's state 0x15 then
6//! clears what is left before the field waits again.
7//!
8//! Original: `FUN_140120620`, `0x140167cb0`, `0x140167db0`, `0x1401678c0`.
9// vtable 0x1401692f0
10
11use std::collections::{BTreeMap, BTreeSet};
12
13use crate::behaviour::character::{Action, Component, Message, MoveTo};
14use crate::pathfind::{nearest_neighbour, WALKABLE};
15use crate::scheduler::{seconds, ticks};
16use crate::town::Sim;
17use crate::work::construction::Callbacks;
18use crate::work::jobs::{Work, WorkKind};
19
20/// `DAT_1401788d4`: the warmth planting and the autumn harvest need (°F).
21const WARM: f32 = 33.0;
22
23// +0x58
24/// One cell of a field (0x20 bytes).
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub struct CropCell {
27    // +0x00
28    /// The map cell.
29    pub cell: i32,
30    // +0x04
31    /// 0 to plant, 1 growing, 2 done (harvested or lost).
32    pub state: i32,
33    // +0x08
34    /// The crop entity.
35    pub entity: Option<usize>,
36    // +0x18
37    /// Harvesting this cell leaves a pile.
38    pub pile: bool,
39}
40
41// +0x48..+0x60
42/// CropFieldComponent.
43#[derive(Debug, Clone, Default, PartialEq)]
44pub struct CropField {
45    // +0x48
46    /// Farming on (the field's toggle).
47    pub enabled: bool,
48    // +0x49
49    /// The crop leaves piles (its natural resource names a raw material).
50    pub piles: bool,
51    // +0x50
52    /// The crop (natural-resource template), chosen in the field's window.
53    pub crop: Option<usize>,
54    /// The field's cells.
55    pub cells: Vec<CropCell>,
56}
57
58// +0x24; +0x20
59/// CropFieldDescription (versioned): a tending visit's growth (as a share of the crop's growing
60/// time) and, from version 1, the stock slot.
61///
62/// Original: `FUN_140120740`.
63#[derive(Debug, Clone, Copy, Default, PartialEq)]
64pub struct CropFieldTemplate {
65    /// A tending visit's growth, as a share of the crop's growing time.
66    pub tend: f32,
67    /// The stock slot (version 1).
68    pub group: i32,
69}
70
71// +0x48; +0x58; +0x68
72/// The work of planting (`PlantArea`) or harvesting a field: the cells claimed (index → worker),
73/// the cells still to do, and, for planting, the crop.
74#[derive(Debug, Clone, Default, PartialEq)]
75pub struct AreaWork {
76    /// The cells claimed: cell index → worker.
77    pub claims: BTreeMap<i32, usize>,
78    /// The cells still to do.
79    pub need: Vec<bool>,
80    /// The crop, for planting.
81    pub crop: Option<usize>,
82}
83
84impl Sim<'_, '_> {
85    fn month(&self) -> i32 {
86        (self.calendar / 3600.0 * 12.0) as i32
87    }
88
89    fn field(&self, e: usize) -> Option<&CropField> {
90        self.sites.get(&e)?.crop_field.as_ref()
91    }
92
93    fn field_mut(&mut self, e: usize) -> Option<&mut CropField> {
94        self.sites.get_mut(&e)?.crop_field.as_mut()
95    }
96
97    fn set_workplace_enabled(&mut self, e: usize, on: bool) {
98        self.enable_workplace(e, on);
99    }
100
101    /// State 2 enter (`FUN_14011fdf0`): the workplace follows the toggle.
102    pub(crate) fn crop_enter2(&mut self, e: usize) {
103        let on = self.field(e).is_some_and(|f| f.enabled);
104        self.set_workplace_enabled(e, on);
105    }
106
107    /// State 2 update (`FUN_14011fd30`): switched on, warm and in spring (months 0–2), plant.
108    pub(crate) fn crop_update2(&mut self, e: usize) {
109        if !self.field(e).is_some_and(|f| f.enabled) || self.temperature < WARM {
110            return;
111        }
112        let month = self.month();
113        if month >= 0 && 8 < (month - 3) as u32 {
114            self.crop_plant_now(e);
115        }
116    }
117
118    /// Plant now (also the field's message 3 in state 2).
119    ///
120    /// Original: `FUN_14011fc80`.
121    pub fn crop_plant_now(&mut self, e: usize) {
122        let on = self.field(e).is_some_and(|f| f.enabled);
123        self.set_workplace_enabled(e, on);
124        self.machine(e).request(9);
125    }
126
127    // +0x58
128    /// CropUI slot.
129    ///
130    /// Original: `FUN_1400b7df0`.
131    pub fn set_crop(&mut self, e: usize, crop: Option<usize>) {
132        let Some(old) = self.field(e).map(|f| f.crop) else {
133            return;
134        };
135        if old.is_none() && crop.is_some() {
136            self.field_action(e, 2);
137        }
138        if let Some(f) = self.field_mut(e) {
139            f.crop = crop;
140        }
141    }
142
143    // +0x70
144    /// CropFieldComponent slot: 0 sends the machine message 3 (handled in state 2: plant now), 1
145    /// message 4 (state 10: harvest now), 2 switches farming on or off.
146    ///
147    /// Original: `FUN_140120190`.
148    pub fn field_action(&mut self, e: usize, action: i32) {
149        let Some(m) = self.sites.get(&e).map(|s| &s.machine) else {
150            return;
151        };
152        let state = m.state;
153        match action {
154            0 if state == 2 => self.crop_plant_now(e),
155            1 if state == 0xa => self.crop_harvest_now(e),
156            2 => {
157                let on = match self.field_mut(e) {
158                    Some(f) => {
159                        f.enabled = !f.enabled;
160                        f.enabled
161                    }
162                    None => return,
163                };
164                self.set_workplace_enabled(e, on);
165            }
166            _ => {}
167        }
168    }
169
170    /// The field's message 4 in state 10: harvest now.
171    ///
172    /// Original: `FUN_14011f3d0`.
173    pub fn crop_harvest_now(&mut self, e: usize) {
174        self.machine(e).request(0xb);
175    }
176
177    /// State 9 enter (`FUN_14011f7e0`): one cell per footprint cell (rows, then columns);
178    /// every `pile_spacing`-th cell leaves a pile, shuffled with `Rand(count)` swaps; then the
179    /// planting work.
180    pub(crate) fn crop_enter9(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
181        let n = self.size as i32;
182        let (x0, y0) = self.placed[e].cell;
183        let (w, h) = self.placed[e].footprint;
184        let mut cells = Vec::with_capacity((w * h) as usize);
185        for row in 0..h {
186            for col in 0..w {
187                cells.push(CropCell {
188                    cell: (row + y0) * n + col + x0,
189                    ..Default::default()
190                });
191            }
192        }
193        let crop = self.field(e).and_then(|f| f.crop);
194        let spacing = crop
195            .and_then(|t| self.templates[t].pile)
196            .and_then(|p| self.templates[p].pile_material)
197            .map(|m| self.materials[m].pile_spacing);
198        let count = cells.len();
199        let mut flags: Vec<bool> = match spacing {
200            Some(k) => (0..count as i32).map(|i| k != 0 && i % k == 0).collect(),
201            None => vec![false; count],
202        };
203        for i in 0..count {
204            let r = self.rng.rand_below(count as u32) as usize;
205            if i != r {
206                flags.swap(i, r);
207            }
208        }
209        for (c, f) in cells.iter_mut().zip(flags) {
210            c.pile = f;
211        }
212        if let Some(field) = self.field_mut(e) {
213            field.piles = spacing.is_some();
214            field.cells = cells;
215        }
216        // `FUN_140129c20`: the field's footprint is lifted, so crops can stand on it.
217        self.unstamp(e);
218        let profession = self.profession_of(e);
219        let work = Work::new(
220            e,
221            self.rect_cell(e),
222            profession,
223            WorkKind::PlantArea(AreaWork {
224                claims: BTreeMap::new(),
225                need: vec![true; count],
226                crop,
227            }),
228        );
229        self.set_work(jobs, e, work, 1, false);
230    }
231
232    /// The workplace's profession (slot `+0x50` of the works).
233    fn profession_of(&self, e: usize) -> usize {
234        self.workforce
235            .workplaces
236            .get(&e)
237            .and_then(|w| w.profession)
238            .unwrap_or(self.professions.laborer)
239    }
240
241    /// `FUN_14011fef0`: a growing cell whose crop has gone (or been replaced) is done.
242    fn crop_refresh(&mut self, e: usize) {
243        let objects = &self.cells.objects;
244        let placed = &self.placed;
245        let templates = self.templates;
246        let Some(field) = self.sites.get_mut(&e).and_then(|s| s.crop_field.as_mut()) else {
247            return;
248        };
249        for c in field.cells.iter_mut().filter(|c| c.state == 1) {
250            let here = objects[c.cell as usize];
251            let alive = here.is_some_and(|o| {
252                templates[placed[o].template].natural.is_some() && Some(o) == c.entity
253            });
254            if !alive {
255                c.state = 2;
256                c.entity = None;
257            }
258        }
259    }
260
261    fn crop_counts(&mut self, e: usize) -> (usize, usize, usize) {
262        self.crop_refresh(e);
263        let Some(field) = self.field(e) else {
264            return (0, 0, 0);
265        };
266        let growing = field.cells.iter().filter(|c| c.state == 1).count();
267        let done = field.cells.iter().filter(|c| c.state == 2).count();
268        (field.cells.len(), growing, done)
269    }
270
271    /// State 9 update (`FUN_14011f6b0`): everything lost → clear (0x15); everything planted
272    /// or lost → grow (10).
273    pub(crate) fn crop_update9(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
274        let (total, growing, done) = self.crop_counts(e);
275        if done == total {
276            self.remove_work(jobs, e);
277            self.machine(e).request(0x15);
278        } else if growing + done == total {
279            self.remove_work(jobs, e);
280            self.machine(e).request(10);
281        }
282    }
283
284    /// State 10 enter (`FUN_14011f5d0`): the tending work.
285    pub(crate) fn crop_enter10(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
286        let profession = self.profession_of(e);
287        let work = Work::new(
288            e,
289            self.rect_cell(e),
290            profession,
291            WorkKind::TendArea(crate::production::herbalist::HerbalistWork::default()),
292        );
293        self.set_work(jobs, e, work, 1, false);
294    }
295
296    /// State 10 exit (`FUN_14011f440`).
297    pub(crate) fn crop_exit10(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
298        let id = self.sites[&e].work_id;
299        if id >= 0 && jobs.works.contains_key(&id) {
300            self.remove_work(jobs, e);
301        }
302    }
303
304    /// `FUN_1401208e0`: the mean growth of the growing cells (at most 1 each) over all cells.
305    fn crop_ripeness(&mut self, e: usize) -> f32 {
306        self.crop_counts(e);
307        self.field(e).map_or(0.0, |f| ripeness(f, self.placed))
308    }
309
310    /// State 10 update (`FUN_14011f4a0`): everything lost → clear; with piles and switched
311    /// on, harvest when ripe, or when warm in months 6–11.
312    pub(crate) fn crop_update10(&mut self, e: usize) {
313        let (total, _, done) = self.crop_counts(e);
314        if done == total {
315            self.machine(e).request(0x15);
316        }
317        let Some(field) = self.field(e) else { return };
318        if !field.enabled || !field.piles {
319            return;
320        }
321        let month = self.month();
322        let autumn = self.temperature >= WARM && month >= 0 && (month - 6) as u32 <= 5;
323        if !autumn && self.crop_ripeness(e) <= 0.99 {
324            return;
325        }
326        self.crop_harvest_now(e);
327    }
328
329    /// State 11 enter (`FUN_14011f330`): the harvest work (`FUN_1400f2990`).
330    pub(crate) fn crop_enter11(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
331        let count = self.field(e).map_or(0, |f| f.cells.len());
332        let profession = self.profession_of(e);
333        let work = Work::new(
334            e,
335            self.rect_cell(e),
336            profession,
337            WorkKind::Harvest(AreaWork {
338                claims: BTreeMap::new(),
339                need: vec![true; count],
340                crop: None,
341            }),
342        );
343        self.set_work(jobs, e, work, 1, false);
344    }
345
346    /// State 11 update (`FUN_14011f250`): everything harvested → clear.
347    pub(crate) fn crop_update11(&mut self, jobs: &mut crate::work::jobs::Jobs, e: usize) {
348        let (total, _, done) = self.crop_counts(e);
349        if done == total {
350            self.remove_work(jobs, e);
351            self.machine(e).request(0x15);
352        }
353    }
354
355    // +0x2d
356    /// CropFieldComponent's message slot. Message 0: cell `index` was planted (its crop keeps the
357    /// cell's pile flag, NaturalResource); message 1: a tending visit advances the cell's crop by
358    /// `growing time × 300 s × tend`.
359    ///
360    /// Original: `FUN_140120020`.
361    pub fn crop_message(&mut self, e: usize, message: i32, index: i32) {
362        let Some(field) = self.field(e) else { return };
363        let Some(&c) = field.cells.get(index as usize) else {
364            return;
365        };
366        let here = self.cells.objects[c.cell as usize];
367        match message {
368            0 => {
369                if let Some(o) = here {
370                    self.keep_piles.insert(o, c.pile);
371                }
372                if let Some(cell) = self
373                    .field_mut(e)
374                    .and_then(|f| f.cells.get_mut(index as usize))
375                {
376                    cell.state = 1;
377                    cell.entity = here;
378                }
379            }
380            1 => {
381                let Some(o) = here else { return };
382                let tend = self.site_templates[self.sites[&e].template]
383                    .crop_field
384                    .map_or(0.0, |d| d.tend);
385                let template = self.placed[o].template;
386                let Some(g) = self.templates[template].growth.clone() else {
387                    return;
388                };
389                let dt = ticks(g.f28 * 300.0 * tend);
390                let temperature = self.temperature;
391                let p = &mut self.placed[o];
392                let Some(state) = &mut p.growth_state else {
393                    return;
394                };
395                let result = state.update(&g, &mut p.growth, dt, temperature, false);
396                // A terminal transition reached by tending, as the scheduled update handles
397                // it (`FUN_140125940`): the map byte, then the crop falls or goes (on the next
398                // animation pass).
399                if let Some(adjustment) = result.map_adjustment {
400                    let n = self.size;
401                    let (x, y) = self.placed[o].cell;
402                    let i = y as usize * n + x as usize;
403                    self.cells.growth_bytes[i] = crate::nature::growth::adjust_map_byte(
404                        self.cells.growth_bytes[i],
405                        adjustment,
406                    );
407                    self.cells.previous_template[i] = Some(template);
408                    let falls = self.templates[template]
409                        .natural
410                        .as_ref()
411                        .is_some_and(|nr| nr.flag);
412                    let p = &mut self.placed[o];
413                    p.removal_requested = true;
414                    if falls {
415                        p.animation = self.templates[template]
416                            .animations
417                            .get(p.variant)
418                            .and_then(Option::as_ref)
419                            .and_then(|g| g.clips.get(2))
420                            .map(|_| crate::behaviour::animation::Playback::new(2, 0));
421                    }
422                    *self.models_dirty = true;
423                }
424            }
425            _ => {}
426        }
427    }
428
429    /// PlantArea slot `+0x20` (`FUN_1400f94e0`).
430    pub(crate) fn plant_try(&mut self, work: &mut Work, c: usize) -> u8 {
431        let e = work.entity;
432        let WorkKind::PlantArea(pa) = &work.kind else {
433            return 0;
434        };
435        if self.work_max(e) < pa.claims.len() as i32 {
436            return 1;
437        }
438        let Some(field) = self.field(e).cloned() else {
439            return 0;
440        };
441        let from = self.citizen_cell(c);
442        let count = pa.need.len() as i32;
443        let mut nothing = true;
444        let mut i = 0;
445        while i < count {
446            let pa = match &work.kind {
447                WorkKind::PlantArea(pa) => pa,
448                _ => return 0,
449            };
450            if !pa.need[i as usize] {
451                i += 1;
452                continue;
453            }
454            nothing = false;
455            if pa.claims.contains_key(&i) {
456                i += 1;
457                continue;
458            }
459            let j = i + 1;
460            let pair = j < count && pa.need[j as usize] && !pa.claims.contains_key(&j);
461            let cell_i = field.cells[i as usize].cell;
462            let cell_j = if pair {
463                field.cells[j as usize].cell
464            } else {
465                -1
466            };
467            let Some(nb) = nearest_neighbour(self.size, &self.cells.types, cell_i, from, 0xe0f)
468            else {
469                i += 1;
470                continue;
471            };
472            if !self.reachable(from, nb) {
473                i += 1;
474                continue;
475            }
476            let crop = pa.crop;
477            let point = self.ground_at(nb);
478            let param = (if pair { j } else { count }) << 16 | i;
479            let cells: Vec<(i32, i32)> = if pair {
480                vec![(cell_i, i), (cell_j, j)]
481            } else {
482                vec![(cell_i, i)]
483            };
484            self.plant_crop_sequence(c, work, e, crop, point, &cells, param);
485            self.took_job(c, work);
486            if let WorkKind::PlantArea(pa) = &mut work.kind {
487                pa.claims.insert(i, c);
488                pa.need[i as usize] = false;
489                if pair {
490                    pa.claims.insert(j, c);
491                    pa.need[j as usize] = false;
492                }
493            }
494            return 2;
495        }
496        if nothing {
497            let idle = matches!(&work.kind, WorkKind::PlantArea(pa) if pa.claims.is_empty());
498            if idle {
499                self.crop_planted(e);
500            }
501        }
502        1
503    }
504
505    /// `FUN_140120860`: planting is over; cells never planted count as done.
506    fn crop_planted(&mut self, e: usize) {
507        let m = &self.sites[&e].machine;
508        let state = if m.pending < 0 { m.state } else { m.pending };
509        if state != 9 {
510            return;
511        }
512        if let Some(f) = self.field_mut(e) {
513            for c in f.cells.iter_mut().filter(|c| c.state == 0) {
514                c.state = 2;
515            }
516        }
517    }
518
519    /// `FUN_1400c31f0` as PlantArea calls it: take the hoe, walk to the cell, work 2.1 s,
520    /// plant each cell (the field occupies it) and tell the field, put the hoe away, report.
521    #[allow(clippy::too_many_arguments)]
522    fn plant_crop_sequence(
523        &mut self,
524        c: usize,
525        work: &mut Work,
526        field: usize,
527        crop: Option<usize>,
528        point: [f32; 3],
529        cells: &[(i32, i32)],
530        param: i32,
531    ) {
532        self.clear_citizen(c, Some(work));
533        self.on_empty_think(c);
534        self.push(
535            c,
536            Action::CompleteTask {
537                work: work.id,
538                param,
539            },
540        );
541        self.push(c, Action::ShowTool(-1));
542        for &(cell, index) in cells {
543            self.push(
544                c,
545                Action::SendMessage(Message {
546                    component: Component::CropField,
547                    message: 0,
548                    entity: Some(field),
549                    a: index,
550                    b: 0,
551                }),
552            );
553            if let Some(t) = crop {
554                self.push(
555                    c,
556                    Action::CreateNaturalResource {
557                        template: t,
558                        occupant: Some(field),
559                        cell,
560                    },
561                );
562            }
563        }
564        self.push(c, self.perform(c, -1000.0, 0xc, 2.1, true));
565        let walk = MoveTo::new(point, 3, 0, WALKABLE, 0, -1.0, self.rng);
566        self.push(c, Action::move_to(walk));
567        self.push(c, Action::ShowTool(3));
568    }
569
570    /// TendArea slot `+0x20` (`FUN_1400fa960`) for a crop field.
571    pub(crate) fn tend_try(&mut self, work: &mut Work, c: usize) -> u8 {
572        let e = work.entity;
573        let WorkKind::TendArea(t) = &work.kind else {
574            return 0;
575        };
576        if self.work_max(e) < t.workers.len() as i32 {
577            return 1;
578        }
579        let from = self.citizen_cell(c);
580        let Some(field) = self.field(e) else { return 0 };
581        let count = field.cells.len() as u32;
582        let index = self.rng.rand_below(count) as i32;
583        let Some(cell) = self
584            .field(e)
585            .and_then(|f| f.cells.get(index as usize))
586            .map(|c| c.cell)
587        else {
588            return 0;
589        };
590        let Some(nb) = nearest_neighbour(self.size, &self.cells.types, cell, from, WALKABLE) else {
591            return 0;
592        };
593        if !self.reachable(from, nb) {
594            return 0;
595        }
596        let at = self.ground_at(cell);
597        let stand = self.ground_at(nb);
598        let message = Message {
599            component: Component::CropField,
600            message: 1,
601            entity: Some(e),
602            a: index,
603            b: 0,
604        };
605        self.tend_sequence(c, work, stand, at, message);
606        self.took_job(c, work);
607        if let WorkKind::TendArea(t) = &mut work.kind {
608            t.workers.insert(c);
609        }
610        2
611    }
612
613    /// `FUN_1400c0570`: take the hoe, walk next to the cell, work 10 s facing it, tell the
614    /// field, put the hoe away, report.
615    pub(crate) fn tend_sequence(
616        &mut self,
617        c: usize,
618        work: &mut Work,
619        stand: [f32; 3],
620        at: [f32; 3],
621        message: Message,
622    ) {
623        self.clear_citizen(c, Some(work));
624        self.on_empty_think(c);
625        self.push(
626            c,
627            Action::CompleteTask {
628                work: work.id,
629                param: 0,
630            },
631        );
632        let heading = crate::behaviour::character::work_facing(at, stand);
633        self.push(c, Action::ShowTool(-1));
634        self.push(c, Action::SendMessage(message));
635        self.push(c, self.perform(c, heading, 0xc, 10.0, true));
636        let walk = MoveTo::new(stand, 3, 0, WALKABLE, 0, -1.0, self.rng);
637        self.push(c, Action::move_to(walk));
638        self.push(c, Action::ShowTool(3));
639    }
640
641    /// The harvest work's slot `+0x20` (`FUN_1400f21a0`) after the AutoPickup offers.
642    pub(crate) fn harvest_try(&mut self, work: &mut Work, c: usize) -> u8 {
643        let e = work.entity;
644        let group = self.site_templates[self.sites[&e].template]
645            .crop_field
646            .map_or(0, |d| d.group);
647        let (count_s, limit) = self.stock[group.clamp(0, 20) as usize];
648        self.set_icon(e, crate::work::professions::icon::LIMIT, limit <= count_s);
649        if limit <= count_s {
650            return 1;
651        }
652        let WorkKind::Harvest(hw) = &work.kind else {
653            return 0;
654        };
655        if self.work_max(e) < hw.claims.len() as i32 {
656            return 1;
657        }
658        let Some(field) = self.field(e).cloned() else {
659            return 0;
660        };
661        let from = self.citizen_cell(c);
662        let count = field.cells.len() as i32;
663        for i in 0..count {
664            let hw = match &work.kind {
665                WorkKind::Harvest(hw) => hw,
666                _ => return 0,
667            };
668            if !hw.need[i as usize] || hw.claims.contains_key(&i) {
669                continue;
670            }
671            let j = i + 1;
672            let mut pair = j < count && hw.need[j as usize] && !hw.claims.contains_key(&j);
673            let Some(first) = self.cells.objects[field.cells[i as usize].cell as usize] else {
674                if let WorkKind::Harvest(hw) = &mut work.kind {
675                    hw.need[i as usize] = false;
676                }
677                continue;
678            };
679            let second = if pair {
680                self.cells.objects[field.cells[j as usize].cell as usize]
681            } else {
682                None
683            };
684            if second.is_none() {
685                pair = false;
686            }
687            let Some(nb) = nearest_neighbour(
688                self.size,
689                &self.cells.types,
690                field.cells[i as usize].cell,
691                from,
692                0xe0f,
693            ) else {
694                continue;
695            };
696            if !self.reachable(from, nb) {
697                continue;
698            }
699            if self.claimed.contains(&first) {
700                continue;
701            }
702            if pair && second.is_some_and(|s| self.claimed.contains(&s)) {
703                pair = false;
704            }
705            let high = if pair { j } else { count };
706            let point = self.ground_at(nb);
707            let mut crops = vec![first];
708            if pair {
709                crops.extend(second);
710            }
711            self.harvest_sequence(c, work, &crops, point, high << 16 | i);
712            self.took_job(c, work);
713            if let WorkKind::Harvest(hw) = &mut work.kind {
714                hw.claims.insert(i, c);
715                hw.need[i as usize] = false;
716                if pair {
717                    hw.claims.insert(j, c);
718                    hw.need[j as usize] = false;
719                }
720            }
721            return 2;
722        }
723        1
724    }
725
726    /// `FUN_1400c6a00`: the removal sequence over one or two crops (no falling animation),
727    /// with the auto-pickup flag.
728    fn harvest_sequence(
729        &mut self,
730        c: usize,
731        work: &mut Work,
732        crops: &[usize],
733        point: [f32; 3],
734        param: i32,
735    ) {
736        let Some(nr) = self.templates[self.placed[crops[0]].template]
737            .natural
738            .clone()
739        else {
740            return;
741        };
742        let facing = crate::behaviour::character::work_facing(self.facing_centre(crops[0]), point);
743        self.clear_citizen(c, Some(work));
744        self.on_empty_think(c);
745        let tool = nr.value as i32;
746        let clip = if nr.value < 11 { tool + 9 } else { 8 } as usize;
747        self.push(
748            c,
749            Action::CompleteTask {
750                work: work.id,
751                param,
752            },
753        );
754        self.push(c, Action::ShowTool(-1));
755        for &o in crops {
756            self.push(
757                c,
758                Action::RemoveResource {
759                    entity: o,
760                    flag: true,
761                },
762            );
763            self.claimed.insert(o);
764        }
765        self.push(c, self.perform(c, facing, clip, seconds(nr.time_a), true));
766        let walk = match tool {
767            0 | 1 | 4..=7 => 2,
768            2 | 3 => 3,
769            _ => 1,
770        };
771        let m = MoveTo::new(point, walk, 0, WALKABLE, 0, -1.0, self.rng);
772        self.push(c, Action::move_to(m));
773        self.push(c, Action::ShowTool(tool));
774    }
775
776    /// PlantArea and harvest slot `+0x30` (`FUN_1400f9430` / `FUN_1400f20f0`): the cells are
777    /// free again.
778    pub(crate) fn area_complete(work: &mut Work, param: i32) {
779        let (WorkKind::PlantArea(a) | WorkKind::Harvest(a)) = &mut work.kind else {
780            return;
781        };
782        let (i, j) = (param & 0xffff, (param >> 16) & 0xffff);
783        a.claims.remove(&i);
784        if (j as usize) < a.need.len() {
785            a.claims.remove(&j);
786        }
787    }
788
789    /// Slot `+0x38` (`FUN_1400f9370` / `FUN_1400f2030`): the cells are to do again.
790    pub(crate) fn area_fail(work: &mut Work, param: i32) {
791        let (WorkKind::PlantArea(a) | WorkKind::Harvest(a)) = &mut work.kind else {
792            return;
793        };
794        let (i, j) = (param & 0xffff, (param >> 16) & 0xffff);
795        a.claims.remove(&i);
796        if let Some(n) = a.need.get_mut(i as usize) {
797            *n = true;
798        }
799        if (j as usize) < a.need.len() {
800            a.need[j as usize] = true;
801            a.claims.remove(&j);
802        }
803    }
804
805    /// Slot `+0x48` (`FUN_1400f9140` / `FUN_1400f1e00`): cells whose worker lost the task are
806    /// to do again.
807    pub(crate) fn area_refresh(&mut self, work: &mut Work) {
808        let id = work.id;
809        let (WorkKind::PlantArea(a) | WorkKind::Harvest(a)) = &mut work.kind else {
810            return;
811        };
812        let lost: Vec<i32> = a
813            .claims
814            .iter()
815            .filter(|(_, &c)| self.characters[c].stack.holds_task(id).is_none())
816            .map(|(&i, _)| i)
817            .collect();
818        for i in lost {
819            a.claims.remove(&i);
820            if let Some(n) = a.need.get_mut(i as usize) {
821                *n = true;
822            }
823        }
824    }
825
826    /// The workers of an area work, for the cancel slot (`FUN_1400f9930` / `FUN_1400f2740`).
827    pub(crate) fn area_workers(work: &Work) -> BTreeSet<usize> {
828        match &work.kind {
829            WorkKind::PlantArea(a) | WorkKind::Harvest(a) => a.claims.values().copied().collect(),
830            _ => BTreeSet::new(),
831        }
832    }
833}
834
835/// `Sim::crop_ripeness` after the cells' refresh.
836///
837/// Original: `FUN_14011fef0`.
838pub fn ripeness(field: &CropField, placed: &[crate::nature::seeding::Placed]) -> f32 {
839    if field.cells.is_empty() {
840        return 0.0;
841    }
842    let sum: f32 = field
843        .cells
844        .iter()
845        .filter(|c| c.state == 1)
846        .filter_map(|c| c.entity)
847        .map(|o| placed[o].growth.min(1.0))
848        .sum();
849    sum / field.cells.len() as f32
850}