src/family.rs 34 K · 1022 lines · raw · history

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