src/family.rs 48.4 K · 1408 lines · raw · history

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