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