1use 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
20const WARM: f32 = 33.0;
22
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub struct CropCell {
27 pub cell: i32,
30 pub state: i32,
33 pub entity: Option<usize>,
36 pub pile: bool,
39}
40
41#[derive(Debug, Clone, Default, PartialEq)]
44pub struct CropField {
45 pub enabled: bool,
48 pub piles: bool,
51 pub crop: Option<usize>,
54 pub cells: Vec<CropCell>,
56}
57
58#[derive(Debug, Clone, Copy, Default, PartialEq)]
64pub struct CropFieldTemplate {
65 pub tend: f32,
67 pub group: i32,
69}
70
71#[derive(Debug, Clone, Default, PartialEq)]
75pub struct AreaWork {
76 pub claims: BTreeMap<i32, usize>,
78 pub need: Vec<bool>,
80 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 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 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 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 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 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 pub fn crop_harvest_now(&mut self, e: usize) {
174 self.machine(e).request(0xb);
175 }
176
177 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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
835pub 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}