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