src/family.rs 33.5 K · 1012 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, NAMES, 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<&str> = colonists.iter().map(|c| c.0.name.as_str()).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 = (0..NAMES.len())
632 .find(|&i| !taken.contains(&NAMES[i]))
633 .unwrap_or(count + born);
634 if let Some(name) = NAMES.get(index) {
635 taken.push(name);
636 }
637 let tile = home
638 .interior()
639 .find(|t| !home.bed_tiles().contains(t))
640 .unwrap_or(home.lo);
641 let child = spawn_colonist(
642 &mut commands,
643 &assets,
644 &item_assets,
645 &mut materials,
646 index,
647 tile,
648 );
649 commands.entity(child).insert((
650 Child { born: tick.0 },
651 Family(family),
652 Transform::from_translation(WorldMap::tile_center(tile).extend(1.0))
653 .with_scale(Vec3::splat(CHILD_SCALE)),
654 ));
655 born += 1;
656 log.push(
657 *tick,
658 format!(
659 "{} and {} had a child, {}",
660 parents[0],
661 parents[1],
662 NAMES[index % NAMES.len()]
663 ),
664 );
665 }
666 }
667
668 fn grow_up(
669 mut commands: Commands,
670 tick: Res<SimTick>,
671 mut log: ResMut<ColonyLog>,
672 mut children: Query<(Entity, &Colonist, &Child, &mut Transform)>,
673 ) {
674 if !tick.0.is_multiple_of(TICKS_PER_HOUR) {
675 return;
676 }
677 for (entity, colonist, child, mut transform) in &mut children {
678 if tick.0 < child.born + CHILDHOOD_TICKS {
679 continue;
680 }
681 transform.scale = Vec3::ONE;
682 commands.entity(entity).remove::<Child>();
683 log.push(*tick, format!("{} has grown up", colonist.name));
684 }
685 }
686
687 #[cfg(test)]
688 mod tests {
689 use super::*;
690 use crate::world::Terrain;
691
692 fn singles(app_world: &mut World, tiles: &[UVec2]) -> Vec<Single> {
693 tiles
694 .iter()
695 .map(|&tile| Single {
696 entity: app_world.spawn_empty().id(),
697 tile,
698 family: None,
699 })
700 .collect()
701 }
702
703 fn bond_for(hours: u32, singles: &[Single]) -> Vec<(Entity, Entity)> {
704 let mut affinity = Affinity::default();
705 let mut rng = SimRng::new(3);
706 let mut bonded: Vec<(Entity, Entity)> = Vec::new();
707 for _ in 0..hours {
708 let free: Vec<Single> = singles
709 .iter()
710 .filter(|s| !bonded.iter().any(|&(a, b)| a == s.entity || b == s.entity))
711 .copied()
712 .collect();
713 bonded.extend(bond_hour(&mut affinity, &free, &mut rng));
714 }
715 bonded
716 }
717
718 #[test]
719 fn colonists_who_spend_time_together_become_partners() {
720 let mut world = World::new();
721 let near = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(6, 7)]);
722 assert!(bond_for(BOND_HOURS - 1, &near).is_empty());
723 assert_eq!(bond_for(BOND_HOURS * 4, &near).len(), 1);
724 let apart = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(15, 5)]);
725 assert!(bond_for(BOND_HOURS * 4, &apart).is_empty());
726 }
727
728 #[test]
729 fn everyone_has_at_most_one_partner_and_family_never_pairs() {
730 let mut world = World::new();
731 let mut trio = singles(&mut world, &[UVec2::ONE, UVec2::ONE, UVec2::ONE]);
732 let bonded = bond_for(BOND_HOURS * 10, &trio);
733 assert_eq!(bonded.len(), 1);
734 for single in &mut trio {
735 single.family = Some(Family(1));
736 }
737 assert!(bond_for(BOND_HOURS * 10, &trio).is_empty());
738 }
739
740 fn plan_on(map: &WorldMap, zones: &Zones, occupied: &HashSet<UVec2>) -> Home {
741 HomeContext {
742 map,
743 zones,
744 blueprints: &Blueprints::default(),
745 homes: &Homes::default(),
746 occupied,
747 }
748 .plan(UVec2::new(20, 20))
749 .unwrap()
750 }
751
752 #[test]
753 fn homes_go_on_open_ground_away_from_the_colony_center() {
754 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass);
755 for x in 0..40 {
756 map.set_plant(UVec2::new(x, 14), Some(crate::world::Plant::Tree));
757 }
758 let mut zones = Zones::default();
759 zones.add_tiles(&map, [UVec2::new(20, 20), UVec2::new(21, 20)]);
760 let occupied = HashSet::from([UVec2::new(20, 26)]);
761 let home = plan_on(&map, &zones, &occupied);
762 let center = (home.lo + home.hi).as_ivec2() / 2;
763 assert!((center - IVec2::new(20, 20)).abs().max_element() >= HOME_MIN_DISTANCE);
764 for y in home.lo.y - 1..=home.hi.y + 1 {
765 for x in home.lo.x - 1..=home.hi.x + 1 {
766 let tile = UVec2::new(x, y);
767 assert!(!zones.is_stockpile(tile));
768 if home.contains(tile) {
769 assert!(map.plant(tile.as_ivec2()).is_none());
770 assert!(!occupied.contains(&tile));
771 }
772 }
773 }
774 }
775
776 #[test]
777 fn a_home_is_a_walled_room_with_a_door_and_two_beds() {
778 let home = Home::at(UVec2::new(10, 10));
779 let plan = home.blueprints(UVec2::new(12, 0));
780 let count = |kind: StructureKind| {
781 plan.values()
782 .filter(|k| **k == BuildKind::Structure(kind))
783 .count()
784 };
785 let ring = 2 * (HOME_INTERIOR.x + 2) + 2 * HOME_INTERIOR.y;
786 assert_eq!(
787 count(StructureKind::Wall) + count(StructureKind::Door),
788 ring as usize
789 );
790 assert_eq!(count(StructureKind::Door), 1);
791 assert_eq!(count(StructureKind::Bed), 2);
792 let door = plan
793 .iter()
794 .find(|(_, k)| **k == BuildKind::Structure(StructureKind::Door))
795 .unwrap()
796 .0;
797 assert_eq!(door.y, home.lo.y);
798 for bed in home.bed_tiles() {
799 assert!(home.interior().any(|t| t == bed));
800 }
801 }
802
803 #[test]
804 fn finished_homes_get_a_roof() {
805 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
806 let home = Home::at(UVec2::new(5, 5));
807 let door = UVec2::new(7, 5);
808 for tile in home.ring().filter(|t| *t != door) {
809 map.set_wall(tile, true);
810 }
811 assert!(roofs_needed(&map, &HashSet::new(), &home).is_empty());
812 let roofs = roofs_needed(&map, &HashSet::from([door]), &home);
813 assert_eq!(roofs.len(), (HOME_INTERIOR.x * HOME_INTERIOR.y) as usize);
814 }
815
816 // Gold has one use: a statue in each finished home, which lifts the family's mood. Planning
817 // it is what makes the colony want gold at all. Finished homes also get a wooden floor (a
818 // smaller mood boost), built under beds and statues alike.
819 #[test]
820 fn finished_homes_get_a_statue_and_floor() {
821 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
822 let home = Home::at(UVec2::new(5, 5));
823 let door = UVec2::new(7, 5);
824 for tile in home.ring().filter(|t| *t != door) {
825 map.set_wall(tile, true);
826 }
827 for tile in home.interior() {
828 map.set_roof(tile, true);
829 }
830 let mut app = App::new();
831 app.insert_resource(map)
832 .init_resource::<SimTick>()
833 .init_resource::<Zones>()
834 .init_resource::<Blueprints>()
835 .insert_resource(Homes {
836 by_family: HashMap::from([(1, home)]),
837 ..default()
838 })
839 .init_resource::<ColonyReach>()
840 .add_systems(Update, plan_homes);
841 app.world_mut().spawn((
842 Structure {
843 kind: StructureKind::Door,
844 },
845 TilePos(door),
846 ));
847 app.world_mut()
848 .spawn((Colonist { name: "Ada".into() }, Family(1)));
849 app.update();
850 let blueprints = app.world().resource::<Blueprints>();
851 assert_eq!(
852 blueprints.get(home.statue_tile()),
853 Some(BuildKind::Structure(StructureKind::Statue))
854 );
855 let floors = home
856 .interior()
857 .filter(|t| blueprints.get(*t) == Some(BuildKind::Floor))
858 .count();
859 assert_eq!(floors, home.interior().count() - 1);
860 }
861
862 fn walled(map: &mut WorldMap, home: Home, door: UVec2) -> HashSet<UVec2> {
863 for tile in home.ring().filter(|t| *t != door) {
864 map.set_wall(tile, true);
865 }
866 HashSet::from([door])
867 }
868
869 fn reachable(map: &WorldMap, from: UVec2) -> Reach {
870 Reach::flood(map, from)
871 }
872
873 // New homes keep a walkable tile all around, so any door they get opens onto open ground.
874 // Homes used to be packed wall to wall and their doors ended up facing a neighbour's wall.
875 #[test]
876 fn new_homes_never_touch_existing_walls() {
877 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass);
878 for y in 0..40 {
879 map.set_wall(UVec2::new(27, y), true);
880 }
881 let mut zones = Zones::default();
882 zones.add_tiles(&map, [UVec2::new(20, 20)]);
883 let home = plan_on(&map, &zones, &HashSet::new());
884 for y in home.lo.y - 1..=home.hi.y + 1 {
885 for x in home.lo.x - 1..=home.hi.x + 1 {
886 assert!(!map.is_wall(IVec2::new(x as i32, y as i32)), "{home:?}");
887 }
888 }
889 }
890
891 // A home whose only door faces a wall can't be entered; it gets a new door on a side that
892 // opens onto ground connected to the colony, knocking out a wall first if needed.
893 #[test]
894 fn homes_with_a_blocked_door_get_a_new_one() {
895 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
896 let home = Home::at(UVec2::new(5, 5));
897 let blocked = UVec2::new(7, 5);
898 let doors = walled(&mut map, home, blocked);
899 map.set_wall(UVec2::new(7, 4), true);
900 let colony = UVec2::new(7, 0);
901 let open = reachable(&map, colony);
902 let (site, kind) = door_repair(&map, &doors, &open, &home, colony).unwrap();
903 assert_eq!(kind, BuildKind::Deconstruct);
904 assert!(home.door_sites().any(|t| t == site) && site != blocked);
905 map.set_wall(site, false);
906 let open = reachable(&map, colony);
907 assert_eq!(
908 door_repair(&map, &doors, &open, &home, colony),
909 Some((site, BuildKind::Structure(StructureKind::Door)))
910 );
911 let fine = walled(
912 &mut WorldMap::filled(UVec2::splat(20), Terrain::Grass),
913 home,
914 blocked,
915 );
916 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
917 assert_eq!(
918 door_repair(&map, &fine, &reachable(&map, colony), &home, colony),
919 None
920 );
921 }
922
923 // Blueprints sealed off from the colony can never be built; they only inflate material
924 // targets, so they are dropped (a home's own repairs reopen it and plan again).
925 #[test]
926 fn unreachable_blueprints_are_dropped() {
927 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
928 let pocket = Home::at(UVec2::new(12, 12));
929 for tile in pocket.ring() {
930 map.set_wall(tile, true);
931 }
932 let mut app = App::new();
933 app.insert_resource(map)
934 .init_resource::<SimTick>()
935 .init_resource::<Zones>()
936 .init_resource::<Homes>()
937 .insert_resource(Blueprints(HashMap::from([
938 (UVec2::new(14, 14), BuildKind::Roof),
939 (UVec2::new(2, 2), BuildKind::Roof),
940 (UVec2::new(12, 12), BuildKind::Deconstruct),
941 ])))
942 .init_resource::<ColonyReach>()
943 .add_systems(Update, plan_homes);
944 app.update();
945 let blueprints = app.world().resource::<Blueprints>();
946 assert_eq!(blueprints.get(UVec2::new(14, 14)), None);
947 assert!(blueprints.get(UVec2::new(2, 2)).is_some());
948 assert!(blueprints.get(UVec2::new(12, 12)).is_some());
949 }
950
951 // Couples move into the homes of families that died out before anyone builds anew, and
952 // communal beds beyond the adults who still lack a home are taken down.
953 #[test]
954 fn vacant_homes_are_reused_and_spare_beds_removed() {
955 let mut map = WorldMap::filled(UVec2::splat(30), Terrain::Grass);
956 let old = Home::at(UVec2::new(5, 5));
957 let door = UVec2::new(7, 5);
958 walled(&mut map, old, door);
959 for tile in old.interior() {
960 map.set_roof(tile, true);
961 }
962 let mut app = App::new();
963 app.insert_resource(map)
964 .init_resource::<SimTick>()
965 .init_resource::<Zones>()
966 .init_resource::<Blueprints>()
967 .init_resource::<Homes>()
968 .init_resource::<ColonyReach>()
969 .add_systems(Update, plan_homes);
970 let world = app.world_mut();
971 let structure = |kind| Structure { kind };
972 world.spawn((structure(StructureKind::Door), TilePos(door)));
973 for bed in old.bed_tiles() {
974 world.spawn((structure(StructureKind::Bed), TilePos(bed)));
975 }
976 for x in 20..24 {
977 world.spawn((structure(StructureKind::Bed), TilePos(UVec2::new(x, 20))));
978 }
979 let a = world
980 .spawn((Colonist { name: "Ada".into() }, Family(2)))
981 .id();
982 let b = world
983 .spawn((
984 Colonist {
985 name: "Bram".into(),
986 },
987 Family(2),
988 Partner(a),
989 ))
990 .id();
991 world.entity_mut(a).insert(Partner(b));
992 world.spawn(Colonist {
993 name: "Cleo".into(),
994 });
995 app.update();
996 let homes = app.world().resource::<Homes>();
997 assert_eq!(homes.by_family.get(&2), Some(&old));
998 let blueprints = app.world().resource::<Blueprints>();
999 let removed = blueprints
1000 .0
1001 .values()
1002 .filter(|k| **k == BuildKind::Deconstruct)
1003 .count();
1004 assert_eq!(removed, 3);
1005 assert!(
1006 !blueprints
1007 .0
1008 .values()
1009 .any(|k| *k == BuildKind::Structure(StructureKind::Wall))
1010 );
1011 }
1012 }