src/family.rs 36.5 K · 1083 lines · raw · history

1 use serde::{Deserialize, Serialize};
2 use std::collections::{HashMap, HashSet};
3
4 use bevy::prelude::*;
5
6 use crate::{
7 building::{Blueprints, BuildKind},
8 colonist::{Colonist, ColonistAssets, colonist_name, free_name_index, spawn_colonist},
9 economy::in_colony,
10 items::{Item, ItemAssets},
11 pathfinding::{ColonyReach, Reach, nearest_tile},
12 population::{ColonyLog, GROWTH_FOOD_DAYS, MAX_COLONISTS, food_in_store},
13 rng::SimRng,
14 rooms::{Room, Rooms},
15 sim::{SimSystems, SimTick, TICKS_PER_HOUR},
16 structures::{Structure, StructureKind},
17 world::{Terrain, TilePos, WorldMap},
18 worldgen::WorldGenerated,
19 zones::{Zones, can_zone},
20 };
21
22 pub struct FamilyPlugin;
23
24 impl Plugin for FamilyPlugin {
25 fn build(&self, app: &mut App) {
26 app.init_resource::<Homes>()
27 .init_resource::<Affinity>()
28 .init_resource::<ColonyReach>()
29 .add_systems(Update, clear_families.run_if(on_message::<WorldGenerated>))
30 .add_systems(
31 FixedUpdate,
32 (
33 plan_homes.in_set(SimSystems::Decide),
34 (bond, births, grow_up).chain().in_set(SimSystems::Act),
35 ),
36 );
37 }
38 }
39
40 pub const HOME_INTERIOR: UVec2 = UVec2::new(4, 3);
41 pub const HOME_MIN_DISTANCE: i32 = 6;
42 pub const BOND_RANGE: i32 = 4;
43 pub const BOND_HOURS: u32 = 16;
44 pub const BOND_CHANCE: f32 = 0.2;
45 pub const MAX_CHILDREN: usize = 2;
46 pub const BIRTH_CHANCE: f32 = 0.35;
47 pub const CHILDHOOD_TICKS: u64 = 4 * 24 * TICKS_PER_HOUR;
48 pub const CHILD_SCALE: f32 = 0.6;
49 const BIRTH_HOUR_TICK: u64 = 20 * TICKS_PER_HOUR;
50 const DAY_TICKS: u64 = 24 * TICKS_PER_HOUR;
51 const HOME_PLAN_INTERVAL_TICKS: u64 = 300;
52 const HOME_SEARCH_TILES: usize = 6000;
53 const HOME_CANDIDATES: usize = 40;
54 const MIN_HOME_INTERIOR: UVec2 = UVec2::new(3, 2);
55
56 #[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Hash)]
57 pub struct Family(pub u32);
58
59 #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
60 pub struct Partner(pub Entity);
61
62 #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
63 pub struct Child {
64 pub born: u64,
65 }
66
67 impl Child {
68 pub fn days_left(&self, tick: SimTick) -> u64 {
69 (self.born + CHILDHOOD_TICKS)
70 .saturating_sub(tick.0)
71 .div_ceil(DAY_TICKS)
72 }
73 }
74
75 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
76 pub struct Home {
77 pub lo: UVec2,
78 pub hi: UVec2,
79 }
80
81 impl Home {
82 pub fn at(lo: UVec2) -> Self {
83 Self {
84 lo,
85 hi: lo + HOME_INTERIOR + UVec2::ONE,
86 }
87 }
88
89 pub fn contains(&self, tile: UVec2) -> bool {
90 tile.cmpge(self.lo).all() && tile.cmple(self.hi).all()
91 }
92
93 pub fn interior(&self) -> impl Iterator<Item = UVec2> + use<> {
94 let (lo, hi) = (self.lo + UVec2::ONE, self.hi - UVec2::ONE);
95 (lo.y..=hi.y).flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
96 }
97
98 pub fn ring(&self) -> impl Iterator<Item = UVec2> + use<> {
99 let (lo, hi) = (self.lo, self.hi);
100 (lo.y..=hi.y)
101 .flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
102 .filter(move |t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y)
103 }
104
105 pub fn bed_tiles(&self) -> [UVec2; 2] {
106 let corner = UVec2::new(self.lo.x + 1, self.hi.y - 1);
107 [corner, corner + UVec2::X]
108 }
109
110 pub fn outward(&self, tile: UVec2) -> IVec2 {
111 let t = tile.as_ivec2();
112 let step = if tile.y == self.lo.y {
113 IVec2::NEG_Y
114 } else if tile.y == self.hi.y {
115 IVec2::Y
116 } else if tile.x == self.lo.x {
117 IVec2::NEG_X
118 } else {
119 IVec2::X
120 };
121 t + step
122 }
123
124 fn is_corner(&self, t: UVec2) -> bool {
125 (t.x == self.lo.x || t.x == self.hi.x) && (t.y == self.lo.y || t.y == self.hi.y)
126 }
127
128 pub fn door_sites(&self) -> impl Iterator<Item = UVec2> + use<> {
129 let home = *self;
130 self.ring().filter(move |t| !home.is_corner(*t))
131 }
132
133 pub fn hearth_tile(&self) -> UVec2 {
134 self.hi - UVec2::ONE
135 }
136
137 pub fn statue_tile(&self) -> UVec2 {
138 UVec2::new(self.hi.x - 1, self.lo.y + 1)
139 }
140
141 pub fn door_site(&self, map: &WorldMap, toward: UVec2) -> Option<UVec2> {
142 self.door_sites()
143 .filter(|t| !map.is_wall(t.as_ivec2()))
144 .filter(|t| map.move_cost(self.outward(*t)).is_some())
145 .min_by(|a, b| {
146 a.as_vec2()
147 .distance_squared(toward.as_vec2())
148 .total_cmp(&b.as_vec2().distance_squared(toward.as_vec2()))
149 .then((a.y, a.x).cmp(&(b.y, b.x)))
150 })
151 }
152
153 pub fn blueprints(&self, map: &WorldMap, toward: UVec2) -> HashMap<UVec2, BuildKind> {
154 let door = self.door_site(map, toward).unwrap_or(self.lo);
155 let mut plan: HashMap<UVec2, BuildKind> = self
156 .ring()
157 .filter(|t| !map.is_wall(t.as_ivec2()))
158 .map(|t| (t, BuildKind::Structure(StructureKind::Wall)))
159 .collect();
160 plan.insert(door, BuildKind::Structure(StructureKind::Door));
161 for bed in self.bed_tiles() {
162 plan.insert(bed, BuildKind::Structure(StructureKind::Bed));
163 }
164 plan
165 }
166 }
167
168 #[derive(Resource, Default, Debug, Clone, Serialize, Deserialize)]
169 pub struct Homes {
170 pub by_family: HashMap<u32, Home>,
171 next_family: u32,
172 #[serde(default)]
173 pub vacant: Vec<Home>,
174 }
175
176 impl Homes {
177 pub fn new_family(&mut self) -> Family {
178 self.next_family += 1;
179 Family(self.next_family)
180 }
181
182 pub fn owner(&self, tile: UVec2) -> Option<u32> {
183 self.by_family
184 .iter()
185 .find(|(_, home)| home.contains(tile))
186 .map(|(family, _)| *family)
187 }
188
189 pub fn all(&self) -> impl Iterator<Item = &Home> {
190 self.by_family.values().chain(&self.vacant)
191 }
192
193 pub fn is_home(&self, tile: UVec2) -> bool {
194 self.all().any(|home| home.contains(tile))
195 }
196
197 pub fn of(&self, family: Option<Family>) -> Option<&Home> {
198 self.by_family.get(&family?.0)
199 }
200 }
201
202 #[derive(Resource, Default, Debug)]
203 pub struct Affinity(pub HashMap<(Entity, Entity), u32>);
204
205 fn clear_families(
206 mut homes: ResMut<Homes>,
207 mut affinity: ResMut<Affinity>,
208 mut reach: ResMut<ColonyReach>,
209 ) {
210 reach.0 = None;
211 homes.by_family.clear();
212 homes.vacant.clear();
213 affinity.0.clear();
214 }
215
216 #[derive(Clone, Copy)]
217 pub struct Single {
218 pub entity: Entity,
219 pub tile: UVec2,
220 pub family: Option<Family>,
221 }
222
223 pub fn bond_hour(
224 affinity: &mut Affinity,
225 singles: &[Single],
226 rng: &mut SimRng,
227 ) -> Vec<(Entity, Entity)> {
228 let mut bonded: Vec<(Entity, Entity)> = Vec::new();
229 let taken =
230 |bonded: &[(Entity, Entity)], e: Entity| bonded.iter().any(|&(a, b)| a == e || b == e);
231 for (i, a) in singles.iter().enumerate() {
232 for b in &singles[i + 1..] {
233 let near = (a.tile.as_ivec2() - b.tile.as_ivec2()).abs().max_element() <= BOND_RANGE;
234 let kin = a.family.is_some() && a.family == b.family;
235 if !near || kin {
236 continue;
237 }
238 let key = (a.entity.min(b.entity), a.entity.max(b.entity));
239 let hours = affinity.0.entry(key).or_insert(0);
240 *hours += 1;
241 if *hours >= BOND_HOURS
242 && !taken(&bonded, a.entity)
243 && !taken(&bonded, b.entity)
244 && rng.range_f32(0.0..1.0) < BOND_CHANCE
245 {
246 bonded.push(key);
247 }
248 }
249 }
250 for pair in &bonded {
251 affinity
252 .0
253 .retain(|&(a, b), _| ![a, b].iter().any(|e| *e == pair.0 || *e == pair.1));
254 }
255 bonded
256 }
257
258 #[allow(clippy::type_complexity)]
259 fn bond(
260 mut commands: Commands,
261 tick: Res<SimTick>,
262 mut rng: ResMut<SimRng>,
263 mut homes: ResMut<Homes>,
264 mut affinity: ResMut<Affinity>,
265 mut log: ResMut<ColonyLog>,
266 colonists: Query<(
267 Entity,
268 &Colonist,
269 &TilePos,
270 Option<&Family>,
271 Option<&Partner>,
272 Has<Child>,
273 )>,
274 ) {
275 if !tick.0.is_multiple_of(TICKS_PER_HOUR) {
276 return;
277 }
278 for (entity, _, _, _, partner, _) in &colonists {
279 if let Some(partner) = partner
280 && !colonists.contains(partner.0)
281 {
282 commands.entity(entity).remove::<Partner>();
283 }
284 }
285 let singles: Vec<Single> = colonists
286 .iter()
287 .filter(|(.., partner, child)| partner.is_none() && !child)
288 .map(|(entity, _, tile, family, ..)| Single {
289 entity,
290 tile: tile.0,
291 family: family.copied(),
292 })
293 .collect();
294 for (a, b) in bond_hour(&mut affinity, &singles, &mut rng) {
295 let family = homes.new_family();
296 commands.entity(a).insert((Partner(b), family));
297 commands.entity(b).insert((Partner(a), family));
298 let name = |e: Entity| {
299 colonists
300 .get(e)
301 .map(|c| c.1.name.clone())
302 .unwrap_or_default()
303 };
304 log.push(
305 *tick,
306 format!("{} and {} became partners", name(a), name(b)),
307 );
308 }
309 }
310
311 pub struct HomeContext<'a> {
312 pub rooms: &'a Rooms,
313 pub map: &'a WorldMap,
314 pub zones: &'a Zones,
315 pub blueprints: &'a Blueprints,
316 pub homes: &'a Homes,
317 pub occupied: &'a HashSet<UVec2>,
318 }
319
320 impl HomeContext<'_> {
321 fn claimed(&self, tile: UVec2) -> bool {
322 self.zones.is_stockpile(tile)
323 || self.zones.is_farm(tile)
324 || self.blueprints.get(tile).is_some()
325 || self.homes.is_home(tile)
326 }
327
328 fn open(&self, tile: IVec2) -> bool {
329 self.map.in_bounds(tile)
330 && can_zone(self.map, tile)
331 && self.map.plant(tile).is_none()
332 && !self.map.is_wall(tile)
333 && !self.occupied.contains(&tile.as_uvec2())
334 && !self.claimed(tile.as_uvec2())
335 }
336
337 fn shared_walls(&self, home: Home) -> Option<usize> {
338 if !self.map.in_bounds(home.hi.as_ivec2()) {
339 return None;
340 }
341 let wall = BuildKind::Structure(StructureKind::Wall);
342 let mut shared = 0;
343 for tile in home.ring() {
344 if self.map.is_wall(tile.as_ivec2()) || self.blueprints.get(tile) == Some(wall) {
345 shared += 1;
346 } else if !self.open(tile.as_ivec2()) || self.rooms.contains(tile) {
347 return None;
348 }
349 }
350 let interior_open = home
351 .interior()
352 .all(|t| self.open(t.as_ivec2()) && !self.rooms.contains(t));
353 let door = home.door_sites().any(|t| {
354 let out = home.outward(t);
355 !self.map.is_wall(t.as_ivec2())
356 && self.blueprints.get(t).is_none()
357 && self.map.move_cost(out).is_some()
358 && !self.rooms.contains(out.as_uvec2())
359 && self.blueprints.get(out.as_uvec2()) != Some(wall)
360 });
361 (interior_open && door).then_some(shared)
362 }
363
364 pub fn plan(&self, colony_home: UVec2) -> Option<Home> {
365 let center = (HOME_INTERIOR / 2 + UVec2::ONE).as_ivec2();
366 let mut candidates: Vec<(usize, Home)> = Vec::new();
367 nearest_tile(self.map, colony_home, HOME_SEARCH_TILES, |p| {
368 let lo = p.as_ivec2() - center;
369 if lo.min_element() < 0 {
370 return false;
371 }
372 let home = Home::at(lo.as_uvec2());
373 let middle = (home.lo + home.hi).as_ivec2() / 2;
374 if (middle - colony_home.as_ivec2()).abs().max_element() < HOME_MIN_DISTANCE {
375 return false;
376 }
377 if let Some(shared) = self.shared_walls(home) {
378 candidates.push((shared, home));
379 }
380 candidates.len() >= HOME_CANDIDATES
381 });
382 candidates
383 .into_iter()
384 .enumerate()
385 .max_by_key(|&(order, (shared, _))| (shared, std::cmp::Reverse(order)))
386 .map(|(_, (_, home))| home)
387 }
388 }
389
390 pub fn door_repair(
391 map: &WorldMap,
392 doors: &HashSet<UVec2>,
393 reachable: &Reach,
394 home: &Home,
395 toward: UVec2,
396 ) -> Option<(UVec2, BuildKind)> {
397 let open_outside = |t: &UVec2| {
398 let out = home.outward(*t);
399 out.min_element() >= 0 && reachable.contains(out.as_uvec2())
400 };
401 if home
402 .door_sites()
403 .any(|t| doors.contains(&t) && open_outside(&t))
404 {
405 return None;
406 }
407 let site = home
408 .door_sites()
409 .filter(|t| !doors.contains(t) && open_outside(t))
410 .min_by_key(|t| {
411 let d = t.as_ivec2() - toward.as_ivec2();
412 (map.is_wall(t.as_ivec2()), d.length_squared(), t.y, t.x)
413 })?;
414 let kind = if map.is_wall(site.as_ivec2()) {
415 BuildKind::Deconstruct
416 } else {
417 BuildKind::Structure(StructureKind::Door)
418 };
419 Some((site, kind))
420 }
421
422 pub fn floored(map: &WorldMap, tile: UVec2) -> bool {
423 map.get(tile.as_ivec2()) == Some(Terrain::WoodFloor)
424 }
425
426 pub fn torn_down(map: &WorldMap, doors: &HashSet<UVec2>, home: &Home) -> bool {
427 let ring: Vec<UVec2> = home.ring().collect();
428 let intact = ring
429 .iter()
430 .filter(|t| map.is_wall(t.as_ivec2()) || doors.contains(t))
431 .count();
432 intact * 2 < ring.len()
433 }
434
435 pub fn home_of_room(room: &Room) -> Option<Home> {
436 let (lo, hi) = room.bounds();
437 let size = hi - lo + UVec2::ONE;
438 (room.is_rectangle() && size.cmpge(MIN_HOME_INTERIOR).all() && lo.min_element() > 0).then(
439 || Home {
440 lo: lo - UVec2::ONE,
441 hi: hi + UVec2::ONE,
442 },
443 )
444 }
445
446 pub fn surplus_beds(communal: &[UVec2], homeless_adults: usize, toward: UVec2) -> Vec<UVec2> {
447 let mut beds = communal.to_vec();
448 beds.sort_by_key(|b| {
449 let d = b.as_ivec2() - toward.as_ivec2();
450 (d.length_squared(), b.y, b.x)
451 });
452 beds.split_off(homeless_adults.min(beds.len()))
453 }
454
455 #[allow(clippy::too_many_arguments)]
456 fn plan_homes(
457 tick: Res<SimTick>,
458 map: Res<WorldMap>,
459 zones: Res<Zones>,
460 mut homes: ResMut<Homes>,
461 mut blueprints: ResMut<Blueprints>,
462 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
463 items: Query<&TilePos, With<Item>>,
464 families: Query<(Option<&Family>, Has<Partner>, Has<Child>), With<Colonist>>,
465 mut colony_reach: ResMut<ColonyReach>,
466 mut flood: Local<Option<((u64, UVec2, UVec2), Reach)>>,
467 rooms: Res<Rooms>,
468 ) {
469 if !tick.0.is_multiple_of(HOME_PLAN_INTERVAL_TICKS) {
470 return;
471 }
472 let colony_home = zones.home(map.size / 2);
473 let living: HashSet<u32> = families
474 .iter()
475 .filter_map(|(f, ..)| f)
476 .map(|f| f.0)
477 .collect();
478 let extinct: Vec<u32> = homes
479 .by_family
480 .keys()
481 .filter(|f| !living.contains(f))
482 .copied()
483 .collect();
484 for family in extinct {
485 if let Some(home) = homes.by_family.remove(&family) {
486 homes.vacant.push(home);
487 }
488 }
489 let of_kind = |kind: StructureKind| -> HashSet<UVec2> {
490 structures
491 .iter()
492 .filter(|(_, s)| s.kind == kind)
493 .map(|(pos, _)| pos.0)
494 .collect()
495 };
496 let doors = of_kind(StructureKind::Door);
497 let standing = |home: &Home| {
498 let planned = home.ring().any(|t| blueprints.get(t).is_some());
499 planned || !torn_down(&map, &doors, home)
500 };
501 homes.by_family.retain(|_, home| standing(home));
502 homes.vacant.retain(standing);
503 let statues = of_kind(StructureKind::Statue);
504 let hearths = of_kind(StructureKind::Hearth);
505 let beds = of_kind(StructureKind::Bed);
506 for room in &rooms.rooms {
507 let room_beds = beds.iter().filter(|b| room.tiles.contains(b)).count();
508 if room_beds >= 2
509 && let Some(home) = home_of_room(room)
510 && !homes
511 .all()
512 .any(|h| h.interior().any(|t| room.tiles.contains(&t)))
513 {
514 homes.vacant.push(home);
515 }
516 }
517 let homeless_family = |homes: &Homes| {
518 families
519 .iter()
520 .filter(|(_, partnered, _)| *partnered)
521 .filter_map(|(f, ..)| f)
522 .map(|f| f.0)
523 .filter(|f| !homes.by_family.contains_key(f))
524 .min()
525 };
526 while let Some(family) = homeless_family(&homes)
527 && let Some(index) = (0..homes.vacant.len()).min_by_key(|&i| {
528 let d = homes.vacant[i].lo.as_ivec2() - colony_home.as_ivec2();
529 d.length_squared()
530 })
531 {
532 let home = homes.vacant.swap_remove(index);
533 homes.by_family.insert(family, home);
534 }
535 let anchored = map.move_cost(colony_home.as_ivec2()).is_some();
536 let key = (map.shape_version(), colony_home, map.size);
537 let stale = flood.as_ref().is_none_or(|(cached, _)| *cached != key)
538 || (anchored && colony_reach.0.is_none());
539 if stale {
540 let fresh = Reach::flood(&map, colony_home);
541 colony_reach.0 = anchored.then(|| fresh.clone());
542 *flood = Some((key, fresh));
543 }
544 let reachable = &flood.as_ref().expect("flood computed above").1;
545 blueprints
546 .0
547 .retain(|tile, _| !anchored || reachable.near(*tile));
548 let owned: HashSet<Home> = homes.by_family.values().copied().collect();
549 let door = BuildKind::Structure(StructureKind::Door);
550 let entries: HashSet<UVec2> = blueprints
551 .0
552 .iter()
553 .filter(|(_, kind)| **kind == door)
554 .map(|(tile, _)| *tile)
555 .chain(doors.iter().copied())
556 .collect();
557 for home in homes.all() {
558 if let Some((tile, kind)) = door_repair(&map, &entries, reachable, home, colony_home) {
559 blueprints.0.insert(tile, kind);
560 continue;
561 }
562 if blueprints.0.keys().any(|t| home.contains(*t)) {
563 continue;
564 }
565 let built = home.interior().all(|t| map.is_roofed(t.as_ivec2()));
566 if built && owned.contains(home) {
567 if !hearths.iter().any(|&t| home.contains(t)) {
568 let hearth = BuildKind::Structure(StructureKind::Hearth);
569 blueprints.0.insert(home.hearth_tile(), hearth);
570 }
571 if !statues.iter().any(|&t| home.contains(t)) {
572 let statue = BuildKind::Structure(StructureKind::Statue);
573 blueprints.0.insert(home.statue_tile(), statue);
574 }
575 for tile in home.interior().filter(|t| !floored(&map, *t)) {
576 blueprints.0.entry(tile).or_insert(BuildKind::Floor);
577 }
578 }
579 }
580 let homeless_adults = families
581 .iter()
582 .filter(|(family, _, child)| !child && homes.of(family.copied()).is_none())
583 .count();
584 let communal: Vec<UVec2> = beds
585 .iter()
586 .filter(|b| !homes.is_home(**b) && blueprints.get(**b).is_none() && reachable.contains(**b))
587 .copied()
588 .collect();
589 for bed in surplus_beds(&communal, homeless_adults, colony_home) {
590 blueprints.0.insert(bed, BuildKind::Deconstruct);
591 }
592 let Some(family) = homeless_family(&homes) else {
593 return;
594 };
595 let occupied: HashSet<UVec2> = structures
596 .iter()
597 .map(|(pos, _)| pos.0)
598 .chain(items.iter().map(|pos| pos.0))
599 .collect();
600 let ctx = HomeContext {
601 rooms: &rooms,
602 map: &map,
603 zones: &zones,
604 blueprints: &blueprints,
605 homes: &homes,
606 occupied: &occupied,
607 };
608 if let Some(home) = ctx.plan(colony_home) {
609 let plan = home.blueprints(&map, colony_home);
610 blueprints.0.extend(plan);
611 homes.by_family.insert(family, home);
612 }
613 }
614
615 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
616 fn births(
617 mut commands: Commands,
618 tick: Res<SimTick>,
619 map: Res<WorldMap>,
620 zones: Res<Zones>,
621 homes: Res<Homes>,
622 assets: Res<ColonistAssets>,
623 item_assets: Res<ItemAssets>,
624 mut materials: ResMut<Assets<ColorMaterial>>,
625 mut rng: ResMut<SimRng>,
626 mut log: ResMut<ColonyLog>,
627 items: Query<(&Item, &TilePos)>,
628 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
629 colonists: Query<(&Colonist, Option<&Family>, Has<Partner>, Has<Child>)>,
630 ) {
631 if tick.0 % DAY_TICKS != BIRTH_HOUR_TICK {
632 return;
633 }
634 let count = colonists.iter().count();
635 let colony_home = zones.home(map.size / 2);
636 let food = food_in_store(
637 items
638 .iter()
639 .filter(|(_, pos)| in_colony(colony_home, pos.0))
640 .map(|(item, _)| item),
641 );
642 let mut born = 0;
643 let mut taken: Vec<String> = colonists.iter().map(|c| c.0.name.clone()).collect();
644 let mut families: Vec<(&u32, &Home)> = homes.by_family.iter().collect();
645 families.sort_by_key(|(family, _)| **family);
646 for (&family, home) in families {
647 let members = || {
648 colonists
649 .iter()
650 .filter(move |c| c.1 == Some(&Family(family)))
651 };
652 let parents: Vec<&str> = members()
653 .filter(|c| c.2)
654 .map(|c| c.0.name.as_str())
655 .collect();
656 let children = members().filter(|c| c.3).count();
657 let beds = structures
658 .iter()
659 .filter(|(pos, s)| s.kind == StructureKind::Bed && home.contains(pos.0))
660 .count();
661 let built = home.interior().all(|t| map.is_roofed(t.as_ivec2()));
662 let room = count + born < MAX_COLONISTS && food >= (count + born) as f32 * GROWTH_FOOD_DAYS;
663 if parents.len() < 2 || children >= MAX_CHILDREN || beds < 2 || !built || !room {
664 continue;
665 }
666 if rng.range_f32(0.0..1.0) >= BIRTH_CHANCE {
667 continue;
668 }
669 let index = free_name_index(taken.iter().map(String::as_str));
670 taken.push(colonist_name(index));
671 let tile = home
672 .interior()
673 .find(|t| !home.bed_tiles().contains(t))
674 .unwrap_or(home.lo);
675 let child = spawn_colonist(
676 &mut commands,
677 &assets,
678 &item_assets,
679 &mut materials,
680 index,
681 tile,
682 );
683 commands.entity(child).insert((
684 Child { born: tick.0 },
685 Family(family),
686 Transform::from_translation(WorldMap::tile_center(tile).extend(1.0))
687 .with_scale(Vec3::splat(CHILD_SCALE)),
688 ));
689 born += 1;
690 log.push(
691 *tick,
692 format!(
693 "{} and {} had a child, {}",
694 parents[0],
695 parents[1],
696 colonist_name(index)
697 ),
698 );
699 }
700 }
701
702 fn grow_up(
703 mut commands: Commands,
704 tick: Res<SimTick>,
705 mut log: ResMut<ColonyLog>,
706 mut children: Query<(Entity, &Colonist, &Child, &mut Transform)>,
707 ) {
708 if !tick.0.is_multiple_of(TICKS_PER_HOUR) {
709 return;
710 }
711 for (entity, colonist, child, mut transform) in &mut children {
712 if tick.0 < child.born + CHILDHOOD_TICKS {
713 continue;
714 }
715 transform.scale = Vec3::ONE;
716 commands.entity(entity).remove::<Child>();
717 log.push(*tick, format!("{} has grown up", colonist.name));
718 }
719 }
720
721 #[cfg(test)]
722 mod tests {
723 use super::*;
724 use crate::world::Terrain;
725
726 fn singles(app_world: &mut World, tiles: &[UVec2]) -> Vec<Single> {
727 tiles
728 .iter()
729 .map(|&tile| Single {
730 entity: app_world.spawn_empty().id(),
731 tile,
732 family: None,
733 })
734 .collect()
735 }
736
737 fn bond_for(hours: u32, singles: &[Single]) -> Vec<(Entity, Entity)> {
738 let mut affinity = Affinity::default();
739 let mut rng = SimRng::new(3);
740 let mut bonded: Vec<(Entity, Entity)> = Vec::new();
741 for _ in 0..hours {
742 let free: Vec<Single> = singles
743 .iter()
744 .filter(|s| !bonded.iter().any(|&(a, b)| a == s.entity || b == s.entity))
745 .copied()
746 .collect();
747 bonded.extend(bond_hour(&mut affinity, &free, &mut rng));
748 }
749 bonded
750 }
751
752 #[test]
753 fn colonists_who_spend_time_together_become_partners() {
754 let mut world = World::new();
755 let near = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(6, 7)]);
756 assert!(bond_for(BOND_HOURS - 1, &near).is_empty());
757 assert_eq!(bond_for(BOND_HOURS * 4, &near).len(), 1);
758 let apart = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(15, 5)]);
759 assert!(bond_for(BOND_HOURS * 4, &apart).is_empty());
760 }
761
762 #[test]
763 fn everyone_has_at_most_one_partner_and_family_never_pairs() {
764 let mut world = World::new();
765 let mut trio = singles(&mut world, &[UVec2::ONE, UVec2::ONE, UVec2::ONE]);
766 let bonded = bond_for(BOND_HOURS * 10, &trio);
767 assert_eq!(bonded.len(), 1);
768 for single in &mut trio {
769 single.family = Some(Family(1));
770 }
771 assert!(bond_for(BOND_HOURS * 10, &trio).is_empty());
772 }
773
774 fn plan_on(map: &WorldMap, zones: &Zones, occupied: &HashSet<UVec2>) -> Home {
775 HomeContext {
776 rooms: &Rooms::detect(map, &HashSet::new()),
777 map,
778 zones,
779 blueprints: &Blueprints::default(),
780 homes: &Homes::default(),
781 occupied,
782 }
783 .plan(UVec2::new(20, 20))
784 .unwrap()
785 }
786
787 #[test]
788 fn homes_go_on_open_ground_away_from_the_colony_center() {
789 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass);
790 for x in 0..40 {
791 map.set_plant(UVec2::new(x, 14), Some(crate::world::Plant::Tree));
792 }
793 let mut zones = Zones::default();
794 zones.add_tiles(&map, [UVec2::new(20, 20), UVec2::new(21, 20)]);
795 let occupied = HashSet::from([UVec2::new(20, 26)]);
796 let home = plan_on(&map, &zones, &occupied);
797 let center = (home.lo + home.hi).as_ivec2() / 2;
798 assert!((center - IVec2::new(20, 20)).abs().max_element() >= HOME_MIN_DISTANCE);
799 for y in home.lo.y - 1..=home.hi.y + 1 {
800 for x in home.lo.x - 1..=home.hi.x + 1 {
801 let tile = UVec2::new(x, y);
802 assert!(!zones.is_stockpile(tile));
803 if home.contains(tile) {
804 assert!(map.plant(tile.as_ivec2()).is_none());
805 assert!(!occupied.contains(&tile));
806 }
807 }
808 }
809 }
810
811 #[test]
812 fn a_home_is_a_walled_room_with_a_door_and_two_beds() {
813 let home = Home::at(UVec2::new(10, 10));
814 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
815 let plan = home.blueprints(&map, UVec2::new(12, 0));
816 let count = |kind: StructureKind| {
817 plan.values()
818 .filter(|k| **k == BuildKind::Structure(kind))
819 .count()
820 };
821 let ring = 2 * (HOME_INTERIOR.x + 2) + 2 * HOME_INTERIOR.y;
822 assert_eq!(
823 count(StructureKind::Wall) + count(StructureKind::Door),
824 ring as usize
825 );
826 assert_eq!(count(StructureKind::Door), 1);
827 assert_eq!(count(StructureKind::Bed), 2);
828 let door = plan
829 .iter()
830 .find(|(_, k)| **k == BuildKind::Structure(StructureKind::Door))
831 .unwrap()
832 .0;
833 assert_eq!(door.y, home.lo.y);
834 for bed in home.bed_tiles() {
835 assert!(home.interior().any(|t| t == bed));
836 }
837 }
838
839 // Gold has one use: a statue in each finished home, which lifts the family's mood. Planning
840 // it is what makes the colony want gold at all. Finished homes also get a hearth, where the
841 // family cooks (one campfire can't feed a big colony) and which heats the home, and a wooden
842 // floor (a smaller mood boost), built under beds, statue and hearth alike.
843 #[test]
844 fn finished_homes_get_a_statue_and_floor() {
845 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
846 let home = Home::at(UVec2::new(5, 5));
847 let door = UVec2::new(7, 5);
848 for tile in home.ring().filter(|t| *t != door) {
849 map.set_wall(tile, true);
850 }
851 for tile in home.interior() {
852 map.set_roof(tile, true);
853 }
854 let mut app = App::new();
855 app.insert_resource(map)
856 .init_resource::<SimTick>()
857 .init_resource::<Zones>()
858 .init_resource::<Blueprints>()
859 .insert_resource(Homes {
860 by_family: HashMap::from([(1, home)]),
861 ..default()
862 })
863 .init_resource::<ColonyReach>()
864 .init_resource::<Rooms>()
865 .add_systems(Update, plan_homes);
866 app.world_mut().spawn((
867 Structure {
868 kind: StructureKind::Door,
869 },
870 TilePos(door),
871 ));
872 app.world_mut()
873 .spawn((Colonist { name: "Ada".into() }, Family(1)));
874 app.update();
875 let blueprints = app.world().resource::<Blueprints>();
876 assert_eq!(
877 blueprints.get(home.statue_tile()),
878 Some(BuildKind::Structure(StructureKind::Statue))
879 );
880 let floors = home
881 .interior()
882 .filter(|t| blueprints.get(*t) == Some(BuildKind::Floor))
883 .count();
884 assert_eq!(floors, home.interior().count() - 2);
885 assert_eq!(
886 blueprints.get(home.hearth_tile()),
887 Some(BuildKind::Structure(StructureKind::Hearth))
888 );
889 }
890
891 fn walled(map: &mut WorldMap, home: Home, door: UVec2) -> HashSet<UVec2> {
892 for tile in home.ring().filter(|t| *t != door) {
893 map.set_wall(tile, true);
894 }
895 HashSet::from([door])
896 }
897
898 fn reachable(map: &WorldMap, from: UVec2) -> Reach {
899 Reach::flood(map, from)
900 }
901
902 // Homes reuse walls that are already standing (a shared wall instead of two side by side) and
903 // only plan the missing ones; the door goes on a side that opens onto walkable ground.
904 #[test]
905 fn new_homes_share_existing_walls() {
906 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass);
907 for y in 0..40 {
908 map.set_wall(UVec2::new(27, y), true);
909 }
910 let mut zones = Zones::default();
911 zones.add_tiles(&map, [UVec2::new(20, 20)]);
912 let home = plan_on(&map, &zones, &HashSet::new());
913 assert!(home.lo.x == 27 || home.hi.x == 27, "{home:?}");
914 let plan = home.blueprints(&map, UVec2::new(20, 20));
915 assert!(plan.keys().all(|t| t.x != 27));
916 let door = plan
917 .iter()
918 .find(|(_, k)| **k == BuildKind::Structure(StructureKind::Door))
919 .map(|(t, _)| *t)
920 .unwrap();
921 assert!(map.move_cost(home.outward(door)).is_some());
922 }
923
924 // A home whose only door faces a wall can't be entered; it gets a new door on a side that
925 // opens onto ground connected to the colony, knocking out a wall first if needed.
926 #[test]
927 fn homes_with_a_blocked_door_get_a_new_one() {
928 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
929 let home = Home::at(UVec2::new(5, 5));
930 let blocked = UVec2::new(7, 5);
931 let doors = walled(&mut map, home, blocked);
932 map.set_wall(UVec2::new(7, 4), true);
933 let colony = UVec2::new(7, 0);
934 let open = reachable(&map, colony);
935 let (site, kind) = door_repair(&map, &doors, &open, &home, colony).unwrap();
936 assert_eq!(kind, BuildKind::Deconstruct);
937 assert!(home.door_sites().any(|t| t == site) && site != blocked);
938 map.set_wall(site, false);
939 let open = reachable(&map, colony);
940 assert_eq!(
941 door_repair(&map, &doors, &open, &home, colony),
942 Some((site, BuildKind::Structure(StructureKind::Door)))
943 );
944 let fine = walled(
945 &mut WorldMap::filled(UVec2::splat(20), Terrain::Grass),
946 home,
947 blocked,
948 );
949 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
950 assert_eq!(
951 door_repair(&map, &fine, &reachable(&map, colony), &home, colony),
952 None
953 );
954 }
955
956 // Blueprints sealed off from the colony can never be built; they only inflate material
957 // targets, so they are dropped (a home's own repairs reopen it and plan again).
958 #[test]
959 fn unreachable_blueprints_are_dropped() {
960 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
961 let pocket = Home::at(UVec2::new(12, 12));
962 for tile in pocket.ring() {
963 map.set_wall(tile, true);
964 }
965 let mut app = App::new();
966 app.insert_resource(map)
967 .init_resource::<SimTick>()
968 .init_resource::<Zones>()
969 .init_resource::<Homes>()
970 .insert_resource(Blueprints(HashMap::from([
971 (UVec2::new(14, 14), BuildKind::Roof),
972 (UVec2::new(2, 2), BuildKind::Roof),
973 (UVec2::new(12, 12), BuildKind::Deconstruct),
974 ])))
975 .init_resource::<ColonyReach>()
976 .init_resource::<Rooms>()
977 .add_systems(Update, plan_homes);
978 app.update();
979 let blueprints = app.world().resource::<Blueprints>();
980 assert_eq!(blueprints.get(UVec2::new(14, 14)), None);
981 assert!(blueprints.get(UVec2::new(2, 2)).is_some());
982 assert!(blueprints.get(UVec2::new(12, 12)).is_some());
983 }
984
985 // Couples move into the homes of families that died out before anyone builds anew, and
986 // communal beds beyond the adults who still lack a home are taken down.
987 #[test]
988 fn vacant_homes_are_reused_and_spare_beds_removed() {
989 let mut map = WorldMap::filled(UVec2::splat(30), Terrain::Grass);
990 let old = Home::at(UVec2::new(5, 5));
991 let door = UVec2::new(7, 5);
992 walled(&mut map, old, door);
993 for tile in old.interior() {
994 map.set_roof(tile, true);
995 }
996 let mut app = App::new();
997 let map_for_rooms = map.clone();
998 app.insert_resource(map)
999 .init_resource::<SimTick>()
1000 .init_resource::<Zones>()
1001 .init_resource::<Blueprints>()
1002 .init_resource::<Homes>()
1003 .init_resource::<ColonyReach>()
1004 .insert_resource(Rooms::detect(&map_for_rooms, &HashSet::from([door])))
1005 .add_systems(Update, plan_homes);
1006 let world = app.world_mut();
1007 let structure = |kind| Structure { kind };
1008 world.spawn((structure(StructureKind::Door), TilePos(door)));
1009 for bed in old.bed_tiles() {
1010 world.spawn((structure(StructureKind::Bed), TilePos(bed)));
1011 }
1012 for x in 20..24 {
1013 world.spawn((structure(StructureKind::Bed), TilePos(UVec2::new(x, 20))));
1014 }
1015 let a = world
1016 .spawn((Colonist { name: "Ada".into() }, Family(2)))
1017 .id();
1018 let b = world
1019 .spawn((
1020 Colonist {
1021 name: "Bram".into(),
1022 },
1023 Family(2),
1024 Partner(a),
1025 ))
1026 .id();
1027 world.entity_mut(a).insert(Partner(b));
1028 world.spawn(Colonist {
1029 name: "Cleo".into(),
1030 });
1031 app.update();
1032 let homes = app.world().resource::<Homes>();
1033 assert_eq!(homes.by_family.get(&2), Some(&old));
1034 let blueprints = app.world().resource::<Blueprints>();
1035 let removed = blueprints
1036 .0
1037 .values()
1038 .filter(|k| **k == BuildKind::Deconstruct)
1039 .count();
1040 assert_eq!(removed, 3);
1041 assert!(
1042 !blueprints
1043 .0
1044 .values()
1045 .any(|k| *k == BuildKind::Structure(StructureKind::Wall))
1046 );
1047 }
1048
1049 // When the player demolishes a home, the family stops treating it as theirs (and plans a new
1050 // one) once most of its walls are gone; otherwise colonists kept putting doors in the gaps.
1051 #[test]
1052 fn torn_down_homes_are_forgotten() {
1053 let map = WorldMap::filled(UVec2::splat(30), Terrain::Grass);
1054 let home = Home::at(UVec2::new(5, 5));
1055 let mut standing = map.clone();
1056 for tile in home.ring() {
1057 standing.set_wall(tile, true);
1058 }
1059 let mut ruin = map.clone();
1060 for tile in home.ring().take(4) {
1061 ruin.set_wall(tile, true);
1062 }
1063 let doors = HashSet::new();
1064 assert!(!torn_down(&standing, &doors, &home));
1065 assert!(torn_down(&ruin, &doors, &home));
1066 let mut app = App::new();
1067 app.insert_resource(ruin)
1068 .init_resource::<SimTick>()
1069 .init_resource::<Zones>()
1070 .init_resource::<Blueprints>()
1071 .init_resource::<ColonyReach>()
1072 .insert_resource(Homes {
1073 by_family: HashMap::from([(1, home)]),
1074 ..default()
1075 })
1076 .init_resource::<Rooms>()
1077 .add_systems(Update, plan_homes);
1078 app.world_mut()
1079 .spawn((Colonist { name: "Ada".into() }, Family(1)));
1080 app.update();
1081 assert!(app.world().resource::<Homes>().of(Some(Family(1))) != Some(&home));
1082 }
1083 }