1use std::collections::BTreeMap;
7
8use banished_formats::types::AnimationGroup;
9
10use crate::behaviour::activities::{StorageGeometry, TownView};
11use crate::behaviour::agents::CitizenState;
12use crate::behaviour::ai::CitizenAi;
13use crate::behaviour::character::{work_facing, Action, Character, Component, Message, MoveTo};
14use crate::pathfind::WALKABLE;
15use crate::people::life::{stored, Home, LifeRules};
16use crate::random::Random;
17use crate::scheduler::ticks;
18use crate::sectors::SectorGraph;
19use crate::terrain::HeightGrid;
20use crate::work::stock::{find_storage, RawMaterial, Storage, StorageSite};
21
22const RETRY: f32 = 3.75;
24const STOCK_UNREACHABLE: f32 = 75.0;
26const STOCK_NOTHING: f32 = 300.0;
27
28pub struct Needs<'a, 'b> {
30 pub town: &'a TownView<'b>,
32 pub sectors: &'a mut SectorGraph,
34 pub sites: &'a [StorageSite],
36 pub storages: &'a mut [Storage],
38 pub materials: &'a [RawMaterial],
40 pub homes: &'a mut BTreeMap<usize, Home>,
42 pub residences: &'a [usize],
48 pub temperature: f32,
50 pub life: &'a LifeRules,
52 pub carry: i32,
55 pub terrain: &'a HeightGrid<'b>,
57 pub size: usize,
59 pub rng: &'a mut Random,
61 pub healers: &'a [usize],
64 pub workplaces: &'a BTreeMap<usize, crate::work::professions::WorkPlace>,
66 pub attendance: &'a mut BTreeMap<usize, crate::work::construction::Attendance>,
68 pub citizens: &'a [crate::people::citizens::Citizen],
70 pub reschool: bool,
75}
76
77pub struct Who<'a> {
79 pub ai: &'a mut CitizenAi,
81 pub ch: &'a mut Character,
83 pub state: &'a mut CitizenState,
85 pub group: &'a AnimationGroup,
87}
88
89impl Needs<'_, '_> {
90 fn point(&self, entity: usize) -> [f32; 3] {
92 let e = &self.town.entities[entity];
93 if let Some(p) = e.use_points.first() {
94 return *p;
95 }
96 crate::town::floor_ground(
97 self.town.floors,
98 self.size,
99 self.terrain,
100 self.town.point_cell(entity),
101 )
102 }
103
104 fn centre(&self, entity: usize) -> [f32; 3] {
106 let e = &self.town.entities[entity];
107 if e.has_model {
108 e.position
109 } else {
110 crate::town::floor_ground(
111 self.town.floors,
112 self.size,
113 self.terrain,
114 self.town.point_cell(entity),
115 )
116 }
117 }
118
119 fn home_food(&self, home: usize) -> i32 {
120 self.homes
121 .get(&home)
122 .map_or(0, |h| stored(&self.storages[h.storage], self.materials, 5))
123 }
124
125 fn warm_home(&self, entity: usize) -> bool {
126 self.homes
127 .get(&entity)
128 .is_some_and(|h| h.warm(self.temperature))
129 }
130
131 fn storage(&self, entity: usize) -> Option<usize> {
133 self.homes.get(&entity).map(|h| h.storage).or_else(|| {
134 self.sites
135 .iter()
136 .find(|s| s.entity == entity)
137 .map(|s| s.storage)
138 })
139 }
140
141 fn search(
143 &mut self,
144 category: u32,
145 cell: i32,
146 home: Option<usize>,
147 ) -> Option<(usize, [f32; 3])> {
148 let home_cell = home.map_or(-1, |h| self.town.entrance(h, self.rng));
149 let (size, terrain) = (self.size, self.terrain);
150 let floors = self.town.floors;
151 let ground = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
152 let mut geometry = StorageGeometry {
153 town: self.town,
154 sectors: self.sectors,
155 ground: &ground,
156 };
157 let found = find_storage(
158 self.sites,
159 self.storages,
160 self.materials,
161 category,
162 cell,
163 home_cell,
164 &mut geometry,
165 self.rng,
166 )?;
167 Some((self.sites[found.site].entity, found.target))
168 }
169
170 fn urgent_search(&mut self, cell: i32, home: Option<usize>) -> Option<(usize, [f32; 3])> {
182 let mut best = i32::MAX;
183 let mut found = None;
184 let (size, terrain) = (self.size, self.terrain);
185 let floors = self.town.floors;
186 let ground_at = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
187 if let Some(h) = home {
188 let entrance = self.town.entrance(h, self.rng);
189 if entrance >= 0 && self.town.reachable(WALKABLE, cell, entrance) {
190 let rank = self.rank(entrance, cell);
191 let food = self
192 .homes
193 .get(&h)
194 .and_then(|r| self.storages[r.storage].find_item(self.materials, 0, self.rng));
195 if food.is_some() {
196 best = rank;
197 found = Some((h, ground_at(entrance)));
198 }
199 }
200 }
201 let (rank, storage) = {
202 let mut geometry = StorageGeometry {
203 town: self.town,
204 sectors: self.sectors,
205 ground: &ground_at,
206 };
207 crate::work::stock::scan_storages(
208 self.sites,
209 self.storages,
210 self.materials,
211 0,
212 cell,
213 None,
214 best,
215 &mut geometry,
216 self.rng,
217 )
218 };
219 if let Some(m) = storage {
220 best = rank;
221 found = Some((self.sites[m.site].entity, m.target));
222 }
223 let residences: Vec<usize> = self.residences.to_vec();
224 for e in residences {
225 let Some(home) = self.homes.get(&e) else {
226 continue;
227 };
228 if home.occupants() > 0 {
229 continue;
230 }
231 let p = self.point(e);
232 let to = self.town.cell_of(p);
233 let rank = self.rank(to, cell);
234 if best <= rank || !self.town.reachable(WALKABLE, cell, to) {
235 continue;
236 }
237 let storage = self.homes[&e].storage;
238 if self.storages[storage]
239 .find_item(self.materials, 0, self.rng)
240 .is_some()
241 && rank >= 0
242 {
243 best = rank;
244 found = Some((e, p));
245 }
246 }
247 found
248 }
249
250 fn begin(&mut self, who: &mut Who) -> crate::behaviour::character::ActionStack {
252 let mut stack = std::mem::take(&mut who.ch.stack);
253 stack.clear(who.ch, who.group, self.storages);
254 stack.set_on_empty(vec![Action::SendMessage(Message {
255 component: Component::CitizenAi,
256 message: 0,
257 entity: None,
258 a: 0,
259 b: 0,
260 })]);
261 stack
262 }
263
264 pub fn eat(&mut self, who: &mut Who) -> bool {
268 let cell = self.town.cell_of(who.ch.position);
269 let home = who.state.home;
270 let mut chosen = None;
271 if self.life.f(0xcc) < who.state.hunger {
272 if let Some(h) = home {
274 if 0 < self.home_food(h) {
275 chosen = Some((h, who.ch.position, self.warm_home(h)));
276 }
277 }
278 if chosen.is_none() {
279 if let Some((e, p)) = self.urgent_search(cell, home) {
280 let warm = self.warm_home(e);
281 chosen = Some((e, p, warm));
282 }
283 }
284 }
285 if chosen.is_none() {
286 if let Some(h) = home {
287 if self.town.can_reach(cell, h, self.rng) && 0 < self.home_food(h) {
288 chosen = Some((h, self.point(h), true));
289 }
290 }
291 }
292 if chosen.is_none() {
293 match self.search(0, cell, home) {
294 Some((e, p)) => {
295 let warm = self.warm_home(e);
296 chosen = Some((e, p, warm));
297 }
298 None => {
299 who.ai.cooldowns[0] = ticks(RETRY);
300 who.ai.cooldowns[14] = ticks(RETRY);
301 return false;
302 }
303 }
304 }
305 let (entity, target, warm) = chosen.unwrap();
306 let facing = work_facing(self.centre(entity), target);
308 let mut stack = self.begin(who);
309 if warm {
310 stack.push(who.ch, who.group, citizen_message(0, entity));
311 }
312 stack.push(who.ch, who.group, citizen_message(2, entity));
313 stack.push(
314 who.ch,
315 who.group,
316 Action::PerformWork {
317 facing,
318 anim: 8,
319 duration: ticks(2.0),
320 elapsed: 0,
321 consume: false,
322 },
323 );
324 let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
325 stack.push(who.ch, who.group, Action::move_to(walk));
326 who.ch.stack = stack;
327 who.state.job = -1;
328 who.state.health_events |= 4;
329 who.state.supply_activity = 0;
330 true
331 }
332
333 pub fn warm(&mut self, who: &mut Who) -> bool {
338 let cell = self.town.cell_of(who.ch.position);
339 let mut chosen = None;
340 if self.life.f(0xec) < who.state.cold {
341 let (size, terrain) = (self.size, self.terrain);
343 let floors = self.town.floors;
344 let ground_at = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
345 let mut best = i32::MAX;
346 let homes: Vec<usize> = self
347 .residences
348 .iter()
349 .copied()
350 .filter(|e| self.homes.contains_key(e))
351 .collect();
352 for e in homes {
353 let p = self.point(e);
354 let to = self.town.cell_of(p);
355 let mut g = StorageGeometry {
356 town: self.town,
357 sectors: self.sectors,
358 ground: &ground_at,
359 };
360 let rank = crate::work::stock::StorageSearchMap::rank(&mut g, to, cell);
361 if rank < best
362 && self.town.reachable(WALKABLE, cell, to)
363 && self.warm_home(e)
364 && rank >= 0
365 {
366 best = rank;
367 chosen = Some((e, p));
368 }
369 }
370 }
371 if chosen.is_none() {
372 if let Some(h) = who.state.home.filter(|&h| self.warm_home(h)) {
373 chosen = Some((h, self.point(h)));
374 }
375 }
376 let Some((entity, target)) = chosen else {
377 who.ai.cooldowns[1] = ticks(RETRY);
378 who.ai.cooldowns[15] = ticks(RETRY);
379 return false;
380 };
381 let mut stack = self.begin(who);
383 stack.push(who.ch, who.group, citizen_message(0, entity));
384 stack.push(
385 who.ch,
386 who.group,
387 Action::PerformWork {
388 facing: -1000.0,
389 anim: 8,
390 duration: ticks(4.0),
391 elapsed: 0,
392 consume: false,
393 },
394 );
395 let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
396 stack.push(who.ch, who.group, Action::move_to(walk));
397 who.ch.stack = stack;
398 who.state.job = -1;
399 who.state.health_events |= 4;
400 who.state.supply_activity = 0;
401 true
402 }
403
404 pub fn stock_home(&mut self, who: &mut Who, fuel: bool, reachable: bool) -> bool {
411 let (slot, category, icon, supply) = if fuel { (7, 1, 7, 2) } else { (6, 0, 6, 1) };
412 let cell = self.town.cell_of(who.ch.position);
413 let Some(home) = who.state.home.filter(|_| reachable) else {
414 who.ai.cooldowns[slot] = ticks(self.life.f(0xd8) * STOCK_UNREACHABLE);
415 return false;
416 };
417 let Some(home_storage) = self.storage(home) else {
418 return false;
419 };
420 let Some((source, from)) = self.search(category, cell, Some(home)) else {
421 who.ai.cooldowns[slot] = ticks(self.life.f(0xd8) * STOCK_NOTHING);
422 who.state.flags |= 4;
423 if stored(
424 &self.storages[home_storage],
425 self.materials,
426 if fuel { 6 } else { 5 },
427 ) == 0
428 {
429 if let Some(h) = self.homes.get_mut(&home) {
430 crate::people::status::set_icon(&mut h.status, icon, true, false);
431 }
432 }
433 return false;
434 };
435 if let Some(h) = self.homes.get_mut(&home) {
436 crate::people::status::set_icon(&mut h.status, icon, false, false);
437 }
438 let Some(source_storage) = self.storage(source) else {
439 return false;
440 };
441 let target = self.point(home);
442 let free = who.state.carried.capacity - who.state.carried.used(self.materials);
444 let mut room = self.carry.max(free);
445 let mut items: Vec<(usize, i32)> = Vec::new();
446 while let Some(m) =
447 self.storages[source_storage].find_item(self.materials, category, self.rng)
448 {
449 let size = self.materials[m].size;
450 if room < size {
451 break;
452 }
453 self.storages[source_storage].reserve(m, 1);
454 match items.iter_mut().find(|(x, _)| *x == m) {
455 Some((_, n)) => *n += 1,
456 None => items.push((m, 1)),
457 }
458 room -= size;
459 }
460 let face_target = work_facing(self.centre(home), target);
462 let face_source = work_facing(self.centre(source), from);
463 let perform = |facing: f32, anim: usize| Action::PerformWork {
464 facing,
465 anim,
466 duration: ticks(0.75),
467 elapsed: 0,
468 consume: false,
469 };
470 let mut stack = self.begin(who);
471 stack.push(who.ch, who.group, citizen_message(0, home));
472 stack.push(who.ch, who.group, perform(face_target, 7));
473 stack.push(who.ch, who.group, Action::ShowTool(-1));
474 stack.push(
475 who.ch,
476 who.group,
477 Action::DropOff {
478 storage: home_storage,
479 material: usize::MAX,
480 count: -1,
481 redirect: true,
483 },
484 );
485 stack.push(who.ch, who.group, perform(face_target, 6));
486 let walk = MoveTo::new(target, 4, 0, WALKABLE, 0, -1.0, self.rng);
487 stack.push(who.ch, who.group, Action::move_to(walk));
488 stack.push(who.ch, who.group, perform(face_source, 7));
489 stack.push(who.ch, who.group, Action::ShowTool(-2));
490 stack.push(
491 who.ch,
492 who.group,
493 Action::PickupStoredRawMaterial {
494 storage: source_storage,
495 items,
496 partial: true,
497 },
498 );
499 stack.push(who.ch, who.group, perform(face_source, 6));
500 let walk = MoveTo::new(from, 1, 0, WALKABLE, 0, -1.0, self.rng);
501 stack.push(who.ch, who.group, Action::move_to(walk));
502 who.ch.stack = stack;
503 who.state.health_events |= 4;
504 who.state.job = -1;
505 who.state.supply_activity = supply;
506 true
507 }
508}
509
510impl Needs<'_, '_> {
511 fn rank(&mut self, a: i32, b: i32) -> i32 {
513 let (size, terrain) = (self.size, self.terrain);
514 let floors = self.town.floors;
515 let ground_at = move |c: i32| crate::town::floor_ground(floors, size, terrain, c);
516 let mut g = StorageGeometry {
517 town: self.town,
518 sectors: self.sectors,
519 ground: &ground_at,
520 };
521 crate::work::stock::StorageSearchMap::rank(&mut g, a, b)
522 }
523
524 fn nearest_point(&mut self, e: usize, cell: i32) -> Option<[f32; 3]> {
526 let points = self.town.entities.get(e)?.use_points.clone();
527 let mut best = 0xffff;
528 let mut found = None;
529 for p in points {
530 let to = self.town.cell_of(p);
531 if !self.town.reachable(WALKABLE, cell, to) {
532 continue;
533 }
534 let rank = self.rank(cell, to);
535 if rank < best {
536 best = rank;
537 found = Some(p);
538 }
539 }
540 found
541 }
542
543 fn visit(&mut self, who: &mut Who, target: [f32; 3], message: i32, secs: f32) {
546 let mut stack = self.begin(who);
547 stack.push(
548 who.ch,
549 who.group,
550 Action::SendMessage(Message {
551 component: Component::Citizen,
552 message,
553 entity: None,
554 a: 0,
555 b: 0,
556 }),
557 );
558 stack.push(
559 who.ch,
560 who.group,
561 Action::PerformWork {
562 facing: -1000.0,
563 anim: 8,
564 duration: ticks(secs),
565 elapsed: 0,
566 consume: false,
567 },
568 );
569 let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
570 stack.push(who.ch, who.group, Action::move_to(walk));
571 who.ch.stack = stack;
572 who.state.health_events |= 4;
573 who.state.job = -1;
574 who.state.supply_activity = 0;
575 }
576
577 fn rest_at_home(&mut self, who: &mut Who, home: usize) {
579 let target = self.point(home);
580 let mut stack = self.begin(who);
581 stack.push(
582 who.ch,
583 who.group,
584 Action::PerformWork {
585 facing: -1000.0,
586 anim: 8,
587 duration: ticks(5.0),
588 elapsed: 0,
589 consume: false,
590 },
591 );
592 stack.push(who.ch, who.group, citizen_message(0, home));
593 let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
594 stack.push(who.ch, who.group, Action::move_to(walk));
595 who.ch.stack = stack;
596 who.state.job = -1;
597 who.state.health_events |= 4;
598 who.state.supply_activity = 0;
599 }
600
601 pub fn school(&mut self, who: &mut Who) -> bool {
606 let Some(school) = who.state.school else {
607 return false;
608 };
609 let cell = self.town.cell_of(who.ch.position);
610 match self.nearest_point(school, cell) {
611 Some(target) => {
612 self.visit(who, target, 4, self.life.f(0x120));
613 true
614 }
615 None => {
616 self.reschool = true;
617 false
618 }
619 }
620 }
621
622 pub fn doctor(&mut self, who: &mut Who, me: usize) -> bool {
628 let cell = self.town.cell_of(who.ch.position);
629 let home = who
630 .state
631 .home
632 .filter(|&h| self.town.can_reach(cell, h, self.rng));
633 let Some(doctor) = who.state.doctor else {
634 return match home {
635 Some(h) => {
636 self.rest_at_home(who, h);
637 true
638 }
639 None => false,
640 };
641 };
642 match self.nearest_point(doctor, cell) {
643 Some(target) => {
644 self.visit(who, target, 5, self.life.f(0x124));
645 false
646 }
647 None => {
648 who.state.doctor = None;
650 if let Some(a) = self.attendance.get_mut(&doctor) {
651 a.attendees.retain(|&x| x != me);
652 }
653 match who.state.home {
654 Some(h) => {
655 self.rest_at_home(who, h);
656 true
657 }
658 None => false,
659 }
660 }
661 }
662 }
663
664 pub fn herbs(&mut self, who: &mut Who) -> bool {
669 who.ai.cooldowns[4] = ticks(75.0);
670 let cell = self.town.cell_of(who.ch.position);
671 let Some((source, from)) = self.search(3, cell, who.state.home) else {
672 return false;
673 };
674 let Some(storage) = self.storage(source) else {
675 return false;
676 };
677 let Some(herb) = self.storages[storage].find_item(self.materials, 3, self.rng) else {
678 return false;
679 };
680 let mut best = 0xffff;
682 let mut healer = None;
683 for &e in self.healers {
684 if self.workplaces.get(&e).is_none_or(|w| w.workers.is_empty()) {
685 continue;
686 }
687 let p = self.point(e);
688 let to = self.town.cell_of(p);
689 if !self.town.reachable(WALKABLE, cell, to) {
690 continue;
691 }
692 let rank = self.rank(cell, to);
693 if rank < best {
694 best = rank;
695 healer = Some((e, p));
696 }
697 }
698 let Some((healer, at)) = healer else {
699 return false;
700 };
701 let face_healer = work_facing(self.centre(healer), at);
703 let face_source = work_facing(self.centre(source), from);
704 self.storages[storage].reserve(herb, 1);
705 let mut stack = self.begin(who);
706 stack.push(who.ch, who.group, citizen_message(3, healer));
707 stack.push(
708 who.ch,
709 who.group,
710 Action::ConsumeStoredRawMaterial {
711 storage: usize::MAX,
712 items: vec![(herb, 1)],
713 reserved: false,
714 },
715 );
716 stack.push(
717 who.ch,
718 who.group,
719 Action::PerformWork {
720 facing: face_healer,
721 anim: 8,
722 duration: ticks(5.0),
723 elapsed: 0,
724 consume: false,
725 },
726 );
727 let walk = MoveTo::new(at, 1, 0, WALKABLE, 0, -1.0, self.rng);
728 stack.push(who.ch, who.group, Action::move_to(walk));
729 stack.push(
730 who.ch,
731 who.group,
732 Action::PickupStoredRawMaterial {
733 storage,
734 items: vec![(herb, 1)],
735 partial: true,
736 },
737 );
738 stack.push(
739 who.ch,
740 who.group,
741 Action::PerformWork {
742 facing: face_source,
743 anim: 8,
744 duration: ticks(1.0),
745 elapsed: 0,
746 consume: false,
747 },
748 );
749 let walk = MoveTo::new(from, 1, 0, WALKABLE, 0, -1.0, self.rng);
750 stack.push(who.ch, who.group, Action::move_to(walk));
751 who.ch.stack = stack;
752 who.state.health_events |= 4;
753 who.state.job = -1;
754 who.state.supply_activity = 0;
755 who.ai.cooldowns[4] = ticks(self.life.f(0x78) * 75.0);
756 true
757 }
758
759 pub fn play(&mut self, who: &mut Who, me: usize) -> bool {
764 let Some(mut g) = self.citizens.get(me).and_then(|c| c.parent) else {
765 return false;
766 };
767 if let Some(p) = self.citizens[g].spouse {
768 if self.rng.rand_below(1) == 0 {
770 g = p;
771 }
772 }
773 let from = self.town.cell_of(self.citizens[g].position);
774 let to = self.town.cell_of(who.ch.position);
775 let Some(cell) =
776 crate::pathfind::nearest_neighbour(self.size, self.town.types, from, to, WALKABLE)
777 else {
778 return false;
779 };
780 let target = crate::town::floor_ground(self.town.floors, self.size, self.terrain, cell);
781 let idle = self.rng.rand_float() * 7.0 + 3.0;
782 let mut stack = self.begin(who);
784 if 0.0 < idle {
785 stack.push(
786 who.ch,
787 who.group,
788 Action::PlayAnimation {
789 anim: 0,
790 duration: ticks(idle),
791 elapsed: 0,
792 },
793 );
794 }
795 let walk = MoveTo::new(target, 1, 0, WALKABLE, 0, -1.0, self.rng);
796 stack.push(who.ch, who.group, Action::move_to(walk));
797 who.ch.stack = stack;
798 true
799 }
800}
801
802fn citizen_message(message: i32, entity: usize) -> Action {
803 Action::SendMessage(Message {
804 component: Component::Citizen,
805 message,
806 entity: Some(entity),
807 a: 0,
808 b: 0,
809 })
810}
811
812pub fn home_has_food(
816 home: &Home,
817 slot: i32,
818 storages: &[Storage],
819 materials: &[RawMaterial],
820 life: &LifeRules,
821) -> bool {
822 let members = home.families.get(slot.max(0) as usize).map_or(0, Vec::len) as i32;
823 home.families_living() * members * life.f(0xc4) as i32
824 <= stored(&storages[home.storage], materials, 5)
825}
826
827pub fn home_has_fuel(
831 home: &Home,
832 storages: &[Storage],
833 materials: &[RawMaterial],
834 life: &LifeRules,
835) -> bool {
836 home.families_living() as f32 * life.f(0xd4)
837 <= stored(&storages[home.storage], materials, 6) as f32
838}