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