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