use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use bevy::prelude::*; use crate::{ building::{Blueprints, BuildKind}, colonist::{Colonist, ColonistAssets, colonist_name, free_name_index, spawn_colonist}, economy::in_colony, items::{Item, ItemAssets}, pathfinding::{ColonyReach, Reach, nearest_tile}, paths::busy_path, population::{ColonyLog, GROWTH_FOOD_DAYS, MAX_COLONISTS, food_in_store}, rng::SimRng, rooms::{Room, Rooms}, sim::{SimSystems, SimTick, TICKS_PER_HOUR}, social::Relations, structures::{Structure, StructureKind}, world::{Terrain, TilePos, WorldMap}, worldgen::WorldGenerated, zones::{Zones, can_zone}, }; pub struct FamilyPlugin; impl Plugin for FamilyPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .init_resource::() .add_systems(Update, clear_families.run_if(on_message::)) .add_systems( FixedUpdate, ( plan_homes.in_set(SimSystems::Decide), (bond, births, grow_up).chain().in_set(SimSystems::Act), ), ); } } pub const HOME_INTERIOR: UVec2 = UVec2::new(4, 3); pub const HOME_MIN_DISTANCE: i32 = 6; pub const BOND_RANGE: i32 = 4; pub const BOND_HOURS: u32 = 16; pub const BOND_CHANCE: f32 = 0.2; pub const MAX_CHILDREN: usize = 2; pub const BIRTH_CHANCE: f32 = 0.35; pub const CHILDHOOD_TICKS: u64 = 4 * 24 * TICKS_PER_HOUR; pub const CHILD_SCALE: f32 = 0.6; const BIRTH_HOUR_TICK: u64 = 20 * TICKS_PER_HOUR; const DAY_TICKS: u64 = 24 * TICKS_PER_HOUR; const HOME_PLAN_INTERVAL_TICKS: u64 = 300; const HOME_SEARCH_TILES: usize = 6000; const HOME_CANDIDATES: usize = 40; const MIN_HOME_INTERIOR: UVec2 = UVec2::new(3, 2); #[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct Family(pub u32); #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)] pub struct Partner(pub Entity); #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)] pub struct Child { pub born: u64, } impl Child { pub fn days_left(&self, tick: SimTick) -> u64 { (self.born + CHILDHOOD_TICKS) .saturating_sub(tick.0) .div_ceil(DAY_TICKS) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Home { pub lo: UVec2, pub hi: UVec2, } impl Home { #[cfg(test)] pub fn at(lo: UVec2) -> Self { Self { lo, hi: lo + HOME_INTERIOR + UVec2::ONE, } } pub fn sized(lo: UVec2, interior: UVec2) -> Self { Self { lo, hi: lo + interior + UVec2::ONE, } } pub fn interior_size(&self) -> UVec2 { self.hi - self.lo - UVec2::ONE } pub fn area(&self) -> u32 { self.interior_size().element_product() } pub fn grown(&self, side: IVec2) -> Option { let lo = self.lo.as_ivec2() + side.min(IVec2::ZERO); let hi = self.hi.as_ivec2() + side.max(IVec2::ZERO); (lo.min_element() >= 0).then(|| Self { lo: lo.as_uvec2(), hi: hi.as_uvec2(), }) } pub fn bed_tiles_for(&self, beds: usize) -> Vec { (0..beds as u32) .map(|i| UVec2::new(self.lo.x + 1 + i, self.hi.y - 1)) .take_while(|t| t.x < self.hi.x - 1 || beds == 1) .collect() } pub fn contains(&self, tile: UVec2) -> bool { tile.cmpge(self.lo).all() && tile.cmple(self.hi).all() } pub fn interior(&self) -> impl Iterator + use<> { let (lo, hi) = (self.lo + UVec2::ONE, self.hi - UVec2::ONE); (lo.y..=hi.y).flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y))) } pub fn ring(&self) -> impl Iterator + use<> { let (lo, hi) = (self.lo, self.hi); (lo.y..=hi.y) .flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y))) .filter(move |t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y) } pub fn bed_tiles(&self) -> [UVec2; 2] { let corner = UVec2::new(self.lo.x + 1, self.hi.y - 1); [corner, corner + UVec2::X] } pub fn outward(&self, tile: UVec2) -> IVec2 { let t = tile.as_ivec2(); let step = if tile.y == self.lo.y { IVec2::NEG_Y } else if tile.y == self.hi.y { IVec2::Y } else if tile.x == self.lo.x { IVec2::NEG_X } else { IVec2::X }; t + step } fn is_corner(&self, t: UVec2) -> bool { (t.x == self.lo.x || t.x == self.hi.x) && (t.y == self.lo.y || t.y == self.hi.y) } pub fn door_sites(&self) -> impl Iterator + use<> { let home = *self; self.ring().filter(move |t| !home.is_corner(*t)) } pub fn hearth_tile(&self) -> UVec2 { self.hi - UVec2::ONE } pub fn statue_tile(&self) -> UVec2 { UVec2::new(self.hi.x - 1, self.lo.y + 1) } pub fn door_site(&self, map: &WorldMap, toward: UVec2) -> Option { self.door_sites() .filter(|t| !map.is_wall(t.as_ivec2())) .filter(|t| map.move_cost(self.outward(*t)).is_some()) .min_by(|a, b| { a.as_vec2() .distance_squared(toward.as_vec2()) .total_cmp(&b.as_vec2().distance_squared(toward.as_vec2())) .then((a.y, a.x).cmp(&(b.y, b.x))) }) } pub fn blueprints( &self, map: &WorldMap, toward: UVec2, beds: usize, ) -> HashMap { let door = self.door_site(map, toward).unwrap_or(self.lo); let mut plan: HashMap = self .ring() .filter(|t| !map.is_wall(t.as_ivec2())) .map(|t| (t, BuildKind::Structure(StructureKind::Wall))) .collect(); plan.insert(door, BuildKind::Structure(StructureKind::Door)); for bed in self.bed_tiles_for(beds) { plan.insert(bed, BuildKind::Structure(StructureKind::Bed)); } plan } } #[derive(Resource, Default, Debug, Clone, Serialize, Deserialize)] pub struct Homes { pub by_family: HashMap, next_family: u32, #[serde(default)] pub vacant: Vec, } impl Homes { pub fn new_family(&mut self) -> Family { self.next_family += 1; Family(self.next_family) } pub fn owner(&self, tile: UVec2) -> Option { self.by_family .iter() .find(|(_, home)| home.contains(tile)) .map(|(family, _)| *family) } pub fn all(&self) -> impl Iterator { self.by_family.values().chain(&self.vacant) } pub fn is_home(&self, tile: UVec2) -> bool { self.all().any(|home| home.contains(tile)) } pub fn of(&self, family: Option) -> Option<&Home> { self.by_family.get(&family?.0) } } #[derive(Resource, Default, Debug)] pub struct Affinity(pub HashMap<(Entity, Entity), u32>); fn clear_families( mut homes: ResMut, mut affinity: ResMut, mut reach: ResMut, ) { reach.0 = None; homes.by_family.clear(); homes.vacant.clear(); affinity.0.clear(); } #[derive(Clone, Copy)] pub struct Single { pub entity: Entity, pub tile: UVec2, pub family: Option, } pub fn bond_hour( affinity: &mut Affinity, singles: &[Single], rng: &mut SimRng, liking: impl Fn(Entity, Entity) -> f32, ) -> Vec<(Entity, Entity)> { let mut bonded: Vec<(Entity, Entity)> = Vec::new(); let taken = |bonded: &[(Entity, Entity)], e: Entity| bonded.iter().any(|&(a, b)| a == e || b == e); for (i, a) in singles.iter().enumerate() { for b in &singles[i + 1..] { let near = (a.tile.as_ivec2() - b.tile.as_ivec2()).abs().max_element() <= BOND_RANGE; let kin = a.family.is_some() && a.family == b.family; let opinion = liking(a.entity, b.entity); if !near || kin || opinion < 0.0 { continue; } let key = (a.entity.min(b.entity), a.entity.max(b.entity)); let hours = affinity.0.entry(key).or_insert(0); *hours += 1; if *hours >= BOND_HOURS && !taken(&bonded, a.entity) && !taken(&bonded, b.entity) && rng.range_f32(0.0..1.0) < BOND_CHANCE * (0.5 + opinion) { bonded.push(key); } } } for pair in &bonded { affinity .0 .retain(|&(a, b), _| ![a, b].iter().any(|e| *e == pair.0 || *e == pair.1)); } bonded } #[allow(clippy::type_complexity)] fn bond( mut commands: Commands, tick: Res, mut rng: ResMut, mut homes: ResMut, mut affinity: ResMut, mut log: ResMut, relations: Res, colonists: Query<( Entity, &Colonist, &TilePos, Option<&Family>, Option<&Partner>, Has, )>, ) { if !tick.0.is_multiple_of(TICKS_PER_HOUR) { return; } for (entity, _, _, _, partner, _) in &colonists { if let Some(partner) = partner && !colonists.contains(partner.0) { commands.entity(entity).remove::(); } } let singles: Vec = colonists .iter() .filter(|(.., partner, child)| partner.is_none() && !child) .map(|(entity, _, tile, family, ..)| Single { entity, tile: tile.0, family: family.copied(), }) .collect(); let liking = |a, b| relations.opinion(a, b); for (a, b) in bond_hour(&mut affinity, &singles, &mut rng, liking) { let family = homes.new_family(); commands.entity(a).insert((Partner(b), family)); commands.entity(b).insert((Partner(a), family)); let name = |e: Entity| { colonists .get(e) .map(|c| c.1.name.clone()) .unwrap_or_default() }; log.push( *tick, format!("{} and {} became partners", name(a), name(b)), ); } } pub struct HomeContext<'a> { pub rooms: &'a Rooms, pub map: &'a WorldMap, pub zones: &'a Zones, pub blueprints: &'a Blueprints, pub homes: &'a Homes, pub occupied: &'a HashSet, } impl HomeContext<'_> { fn claimed(&self, tile: UVec2) -> bool { self.zones.is_stockpile(tile) || self.zones.is_farm(tile) || self.blueprints.get(tile).is_some() || self.homes.is_home(tile) } fn open(&self, tile: IVec2) -> bool { self.map.in_bounds(tile) && can_zone(self.map, tile) && !busy_path(self.map, tile) && self.map.plant(tile).is_none() && !self.map.is_wall(tile) && !self.occupied.contains(&tile.as_uvec2()) && !self.claimed(tile.as_uvec2()) } fn shared_walls(&self, home: Home) -> Option { if !self.map.in_bounds(home.hi.as_ivec2()) { return None; } let wall = BuildKind::Structure(StructureKind::Wall); let mut shared = 0; for tile in home.ring() { if self.map.is_wall(tile.as_ivec2()) || self.blueprints.get(tile) == Some(wall) { shared += 1; } else if !self.open(tile.as_ivec2()) || self.rooms.contains(tile) { return None; } } let interior_open = home .interior() .all(|t| self.open(t.as_ivec2()) && !self.rooms.contains(t)); let door = home.door_sites().any(|t| { let out = home.outward(t); !self.map.is_wall(t.as_ivec2()) && self.blueprints.get(t).is_none() && self.map.move_cost(out).is_some() && !self.rooms.contains(out.as_uvec2()) && self.blueprints.get(out.as_uvec2()) != Some(wall) }); (interior_open && door).then_some(shared) } pub fn expansion(&self, home: Home, desired: UVec2) -> Option<(Home, Vec)> { let size = home.interior_size(); let sides: Vec = if size.x < desired.x { vec![IVec2::X, IVec2::NEG_X] } else if size.y < desired.y { vec![IVec2::Y, IVec2::NEG_Y] } else { vec![IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] }; let wall = BuildKind::Structure(StructureKind::Wall); sides .into_iter() .filter_map(|side| { let grown = home.grown(side)?; if !self.map.in_bounds(grown.hi.as_ivec2()) { return None; } let new_ring: Vec = grown.ring().filter(|t| !home.contains(*t)).collect(); let new_inside: Vec = grown.interior().filter(|t| !home.contains(*t)).collect(); let mut shared = 0; for &t in &new_ring { if self.map.is_wall(t.as_ivec2()) || self.blueprints.get(t) == Some(wall) { shared += 1; } else if !self.open(t.as_ivec2()) || self.rooms.contains(t) { return None; } } let inside_open = new_inside .iter() .all(|t| self.open(t.as_ivec2()) && !self.rooms.contains(*t)); let opened: Vec = home .ring() .filter(|t| grown.interior().any(|i| i == *t)) .collect(); inside_open.then_some((shared, grown, opened)) }) .max_by_key(|(shared, ..)| *shared) .map(|(_, grown, opened)| (grown, opened)) } #[cfg(test)] pub fn plan(&self, colony_home: UVec2) -> Option { self.plan_sized(colony_home, HOME_INTERIOR) } pub fn plan_sized(&self, colony_home: UVec2, interior: UVec2) -> Option { let center = (interior / 2 + UVec2::ONE).as_ivec2(); let mut candidates: Vec<(usize, Home)> = Vec::new(); nearest_tile(self.map, colony_home, HOME_SEARCH_TILES, |p| { let lo = p.as_ivec2() - center; if lo.min_element() < 0 { return false; } let home = Home::sized(lo.as_uvec2(), interior); let middle = (home.lo + home.hi).as_ivec2() / 2; if (middle - colony_home.as_ivec2()).abs().max_element() < HOME_MIN_DISTANCE { return false; } if let Some(shared) = self.shared_walls(home) { candidates.push((shared, home)); } candidates.len() >= HOME_CANDIDATES }); candidates .into_iter() .enumerate() .max_by_key(|&(order, (shared, _))| (shared, std::cmp::Reverse(order))) .map(|(_, (_, home))| home) } } pub fn door_repair( map: &WorldMap, doors: &HashSet, reachable: &Reach, home: &Home, toward: UVec2, ) -> Option<(UVec2, BuildKind)> { let open_outside = |t: &UVec2| { let out = home.outward(*t); out.min_element() >= 0 && reachable.contains(out.as_uvec2()) }; if home .door_sites() .any(|t| doors.contains(&t) && open_outside(&t)) { return None; } let site = home .door_sites() .filter(|t| !doors.contains(t) && open_outside(t)) .min_by_key(|t| { let d = t.as_ivec2() - toward.as_ivec2(); (map.is_wall(t.as_ivec2()), d.length_squared(), t.y, t.x) })?; let kind = if map.is_wall(site.as_ivec2()) { BuildKind::Deconstruct } else { BuildKind::Structure(StructureKind::Door) }; Some((site, kind)) } pub fn floored(map: &WorldMap, tile: UVec2) -> bool { map.get(tile.as_ivec2()) == Some(Terrain::WoodFloor) } pub fn torn_down(map: &WorldMap, doors: &HashSet, home: &Home) -> bool { let ring: Vec = home.ring().collect(); let intact = ring .iter() .filter(|t| map.is_wall(t.as_ivec2()) || doors.contains(t)) .count(); intact * 2 < ring.len() } pub fn desired_interior(adults: usize, children: usize) -> UVec2 { match (adults.max(1), children) { (1, 0) => UVec2::new(3, 2), (_, 0) => HOME_INTERIOR, (_, 1) => UVec2::new(5, 3), _ => UVec2::new(5, 4), } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Household { pub partners: usize, pub children: usize, } impl Household { pub fn adults(&self) -> usize { self.partners.max(1) } pub fn desired(&self) -> UVec2 { desired_interior(self.adults(), self.children) } } pub fn home_of_room(room: &Room) -> Option { let (lo, hi) = room.bounds(); let size = hi - lo + UVec2::ONE; (room.is_rectangle() && size.cmpge(MIN_HOME_INTERIOR).all() && lo.min_element() > 0).then( || Home { lo: lo - UVec2::ONE, hi: hi + UVec2::ONE, }, ) } pub fn surplus_beds(communal: &[UVec2], homeless_adults: usize, toward: UVec2) -> Vec { let mut beds = communal.to_vec(); beds.sort_by_key(|b| { let d = b.as_ivec2() - toward.as_ivec2(); (d.length_squared(), b.y, b.x) }); beds.split_off(homeless_adults.min(beds.len())) } #[allow(clippy::too_many_arguments)] fn plan_homes( mut commands: Commands, tick: Res, map: Res, zones: Res, mut homes: ResMut, mut blueprints: ResMut, structures: Query<(&TilePos, &Structure), Without>, items: Query<&TilePos, With>, families: Query<(Entity, Option<&Family>, Has, Has), With>, mut colony_reach: ResMut, mut flood: Local>, rooms: Res, ) { if !tick.0.is_multiple_of(HOME_PLAN_INTERVAL_TICKS) { return; } let colony_home = zones.home(map.size / 2); let living: HashSet = families .iter() .filter_map(|(_, f, ..)| f) .map(|f| f.0) .collect(); let mut households: HashMap = HashMap::new(); for (_, family, partnered, child) in &families { if let Some(family) = family { let household = households.entry(family.0).or_default(); household.partners += partnered as usize; household.children += child as usize; } } let household = |family: u32| households.get(&family).copied().unwrap_or_default(); let extinct: Vec = homes .by_family .keys() .filter(|f| !living.contains(f)) .copied() .collect(); for family in extinct { if let Some(home) = homes.by_family.remove(&family) { homes.vacant.push(home); } } let of_kind = |kind: StructureKind| -> HashSet { structures .iter() .filter(|(_, s)| s.kind == kind) .map(|(pos, _)| pos.0) .collect() }; let doors = of_kind(StructureKind::Door); let standing = |home: &Home| { let planned = home.ring().any(|t| blueprints.get(t).is_some()); planned || !torn_down(&map, &doors, home) }; homes.by_family.retain(|_, home| standing(home)); homes.vacant.retain(standing); let statues = of_kind(StructureKind::Statue); let hearths = of_kind(StructureKind::Hearth); let beds = of_kind(StructureKind::Bed); for room in &rooms.rooms { let room_beds = beds.iter().filter(|b| room.tiles.contains(b)).count(); if room_beds >= 2 && let Some(home) = home_of_room(room) && !homes .all() .any(|h| h.interior().any(|t| room.tiles.contains(&t))) { homes.vacant.push(home); } } let homeless_family = |homes: &Homes| { families .iter() .filter(|(_, _, partnered, _)| *partnered) .filter_map(|(_, f, ..)| f) .map(|f| f.0) .filter(|f| !homes.by_family.contains_key(f)) .min() }; let pick_vacant = |homes: &Homes, desired: UVec2| { (0..homes.vacant.len()).min_by_key(|&i| { let home = homes.vacant[i]; let d = home.lo.as_ivec2() - colony_home.as_ivec2(); (home.area() < desired.element_product(), d.length_squared()) }) }; while let Some(family) = homeless_family(&homes) && let Some(index) = pick_vacant(&homes, household(family).desired()) { let home = homes.vacant.swap_remove(index); homes.by_family.insert(family, home); } let solo = families .iter() .filter(|(_, family, partnered, child)| { !partnered && !child && family.is_none_or(|f| { household(f.0).partners > 0 || !homes.by_family.contains_key(&f.0) }) }) .map(|(entity, ..)| entity) .min(); let anchored = map.move_cost(colony_home.as_ivec2()).is_some(); let key = (map.shape_version(), colony_home, map.size); let stale = flood.as_ref().is_none_or(|(cached, _)| *cached != key) || (anchored && colony_reach.0.is_none()); if stale { let fresh = Reach::flood(&map, colony_home); colony_reach.0 = anchored.then(|| fresh.clone()); *flood = Some((key, fresh)); } let reachable = &flood.as_ref().expect("flood computed above").1; blueprints .0 .retain(|tile, _| !anchored || reachable.near(*tile)); let owned: HashMap = homes.by_family.iter().map(|(f, h)| (*h, *f)).collect(); let mut expansions: Vec<(u32, Home, Vec)> = Vec::new(); let door = BuildKind::Structure(StructureKind::Door); let entries: HashSet = blueprints .0 .iter() .filter(|(_, kind)| **kind == door) .map(|(tile, _)| *tile) .chain(doors.iter().copied()) .collect(); for home in homes.all() { if let Some((tile, kind)) = door_repair(&map, &entries, reachable, home, colony_home) { blueprints.0.insert(tile, kind); continue; } if blueprints.0.keys().any(|t| home.contains(*t)) { continue; } let built = home.interior().all(|t| map.is_roofed(t.as_ivec2())); if let Some(&family) = owned.get(home).filter(|_| built) { let needs = household(family); let desired = needs.desired(); if home.area() < desired.element_product() { expansions.push((family, *home, Vec::new())); continue; } let bed = BuildKind::Structure(StructureKind::Bed); let have = beds.iter().filter(|b| home.contains(**b)).count(); for tile in home.bed_tiles_for(needs.adults()).into_iter().skip(have) { if !beds.contains(&tile) { blueprints.0.entry(tile).or_insert(bed); } } if !hearths.iter().any(|&t| home.contains(t)) { let hearth = BuildKind::Structure(StructureKind::Hearth); blueprints.0.insert(home.hearth_tile(), hearth); } if !statues.iter().any(|&t| home.contains(t)) { let statue = BuildKind::Structure(StructureKind::Statue); blueprints.0.insert(home.statue_tile(), statue); } for tile in home.interior().filter(|t| !floored(&map, *t)) { blueprints.0.entry(tile).or_insert(BuildKind::Floor); } } } let homeless_adults = families .iter() .filter(|(_, family, _, child)| !child && homes.of(family.copied()).is_none()) .count(); let communal: Vec = beds .iter() .filter(|b| !homes.is_home(**b) && blueprints.get(**b).is_none() && reachable.contains(**b)) .copied() .collect(); for bed in surplus_beds(&communal, homeless_adults, colony_home) { blueprints.0.insert(bed, BuildKind::Deconstruct); } let occupied: HashSet = structures .iter() .map(|(pos, _)| pos.0) .chain(items.iter().map(|pos| pos.0)) .collect(); let ctx = HomeContext { rooms: &rooms, map: &map, zones: &zones, blueprints: &blueprints, homes: &homes, occupied: &occupied, }; let mut grown_homes = Vec::new(); for (family, home, _) in expansions { if let Some((grown, opened)) = ctx.expansion(home, household(family).desired()) { grown_homes.push((family, grown, opened)); } } enum Plan { Family(u32, Home), SoloVacant(Entity, usize), Solo(Entity, Home), } let plan = if let Some(family) = homeless_family(&homes) { ctx.plan_sized(colony_home, household(family).desired()) .map(|home| Plan::Family(family, home)) } else if let Some(entity) = solo { let desired = desired_interior(1, 0); match pick_vacant(&homes, desired) .filter(|&i| homes.vacant[i].area() <= HOME_INTERIOR.element_product()) { Some(index) => Some(Plan::SoloVacant(entity, index)), None => ctx .plan_sized(colony_home, desired) .map(|home| Plan::Solo(entity, home)), } } else { None }; let wall = BuildKind::Structure(StructureKind::Wall); for (family, grown, opened) in grown_homes { for tile in grown.ring().filter(|t| !map.is_wall(t.as_ivec2())) { if !doors.contains(&tile) { blueprints.0.entry(tile).or_insert(wall); } } for tile in opened { if map.is_wall(tile.as_ivec2()) || doors.contains(&tile) { blueprints.0.insert(tile, BuildKind::Deconstruct); } else { blueprints.0.remove(&tile); } } homes.by_family.insert(family, grown); } let (family, home, beds) = match plan { None => return, Some(Plan::Family(family, home)) => (family, home, household(family).adults()), Some(Plan::SoloVacant(entity, index)) => { let family = homes.new_family(); commands.entity(entity).insert(family); let home = homes.vacant.swap_remove(index); homes.by_family.insert(family.0, home); return; } Some(Plan::Solo(entity, home)) => { let family = homes.new_family(); commands.entity(entity).insert(family); (family.0, home, 1) } }; blueprints .0 .extend(home.blueprints(&map, colony_home, beds)); homes.by_family.insert(family, home); } #[allow(clippy::too_many_arguments, clippy::type_complexity)] fn births( mut commands: Commands, tick: Res, map: Res, zones: Res, homes: Res, assets: Res, item_assets: Res, mut materials: ResMut>, mut rng: ResMut, mut log: ResMut, items: Query<(&Item, &TilePos)>, structures: Query<(&TilePos, &Structure), Without>, colonists: Query<(&Colonist, Option<&Family>, Has, Has)>, ) { if tick.0 % DAY_TICKS != BIRTH_HOUR_TICK { return; } let count = colonists.iter().count(); let colony_home = zones.home(map.size / 2); let food = food_in_store( items .iter() .filter(|(_, pos)| in_colony(colony_home, pos.0)) .map(|(item, _)| item), ); let mut born = 0; let mut taken: Vec = colonists.iter().map(|c| c.0.name.clone()).collect(); let mut families: Vec<(&u32, &Home)> = homes.by_family.iter().collect(); families.sort_by_key(|(family, _)| **family); for (&family, home) in families { let members = || { colonists .iter() .filter(move |c| c.1 == Some(&Family(family))) }; let parents: Vec<&str> = members() .filter(|c| c.2) .map(|c| c.0.name.as_str()) .collect(); let children = members().filter(|c| c.3).count(); let beds = structures .iter() .filter(|(pos, s)| s.kind == StructureKind::Bed && home.contains(pos.0)) .count(); let built = home.interior().all(|t| map.is_roofed(t.as_ivec2())); let room = count + born < MAX_COLONISTS && food >= (count + born) as f32 * GROWTH_FOOD_DAYS; if parents.len() < 2 || children >= MAX_CHILDREN || beds < 2 || !built || !room { continue; } if rng.range_f32(0.0..1.0) >= BIRTH_CHANCE { continue; } let index = free_name_index(taken.iter().map(String::as_str)); taken.push(colonist_name(index)); let tile = home .interior() .find(|t| !home.bed_tiles().contains(t)) .unwrap_or(home.lo); let child = spawn_colonist( &mut commands, &assets, &item_assets, &mut materials, index, tile, ); commands.entity(child).insert(( Child { born: tick.0 }, Family(family), Transform::from_translation(WorldMap::tile_center(tile).extend(1.0)) .with_scale(Vec3::splat(CHILD_SCALE)), )); born += 1; log.push( *tick, format!( "{} and {} had a child, {}", parents[0], parents[1], colonist_name(index) ), ); } } fn grow_up( mut commands: Commands, tick: Res, mut log: ResMut, mut children: Query<(Entity, &Colonist, &Child, &mut Transform)>, ) { if !tick.0.is_multiple_of(TICKS_PER_HOUR) { return; } for (entity, colonist, child, mut transform) in &mut children { if tick.0 < child.born + CHILDHOOD_TICKS { continue; } transform.scale = Vec3::ONE; commands.entity(entity).remove::(); log.push(*tick, format!("{} has grown up", colonist.name)); } } #[cfg(test)] mod tests { use super::*; use crate::world::Terrain; fn singles(app_world: &mut World, tiles: &[UVec2]) -> Vec { tiles .iter() .map(|&tile| Single { entity: app_world.spawn_empty().id(), tile, family: None, }) .collect() } fn bond_for(hours: u32, singles: &[Single]) -> Vec<(Entity, Entity)> { let mut affinity = Affinity::default(); let mut rng = SimRng::new(3); let mut bonded: Vec<(Entity, Entity)> = Vec::new(); for _ in 0..hours { let free: Vec = singles .iter() .filter(|s| !bonded.iter().any(|&(a, b)| a == s.entity || b == s.entity)) .copied() .collect(); bonded.extend(bond_hour(&mut affinity, &free, &mut rng, |_, _| 0.5)); } bonded } #[test] fn colonists_who_spend_time_together_become_partners() { let mut world = World::new(); let near = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(6, 7)]); assert!(bond_for(BOND_HOURS - 1, &near).is_empty()); assert_eq!(bond_for(BOND_HOURS * 4, &near).len(), 1); let apart = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(15, 5)]); assert!(bond_for(BOND_HOURS * 4, &apart).is_empty()); } #[test] fn everyone_has_at_most_one_partner_and_family_never_pairs() { let mut world = World::new(); let mut trio = singles(&mut world, &[UVec2::ONE, UVec2::ONE, UVec2::ONE]); let bonded = bond_for(BOND_HOURS * 10, &trio); assert_eq!(bonded.len(), 1); for single in &mut trio { single.family = Some(Family(1)); } assert!(bond_for(BOND_HOURS * 10, &trio).is_empty()); } fn plan_on(map: &WorldMap, zones: &Zones, occupied: &HashSet) -> Home { HomeContext { rooms: &Rooms::detect(map, &HashSet::new()), map, zones, blueprints: &Blueprints::default(), homes: &Homes::default(), occupied, } .plan(UVec2::new(20, 20)) .unwrap() } #[test] fn homes_go_on_open_ground_away_from_the_colony_center() { let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass); for x in 0..40 { map.set_plant(UVec2::new(x, 14), Some(crate::world::Plant::Tree)); } let mut zones = Zones::default(); zones.add_tiles(&map, [UVec2::new(20, 20), UVec2::new(21, 20)]); let occupied = HashSet::from([UVec2::new(20, 26)]); let home = plan_on(&map, &zones, &occupied); let center = (home.lo + home.hi).as_ivec2() / 2; assert!((center - IVec2::new(20, 20)).abs().max_element() >= HOME_MIN_DISTANCE); for y in home.lo.y - 1..=home.hi.y + 1 { for x in home.lo.x - 1..=home.hi.x + 1 { let tile = UVec2::new(x, y); assert!(!zones.is_stockpile(tile)); if home.contains(tile) { assert!(map.plant(tile.as_ivec2()).is_none()); assert!(!occupied.contains(&tile)); } } } } #[test] fn a_home_is_a_walled_room_with_a_door_and_two_beds() { let home = Home::at(UVec2::new(10, 10)); let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); let plan = home.blueprints(&map, UVec2::new(12, 0), 2); let count = |kind: StructureKind| { plan.values() .filter(|k| **k == BuildKind::Structure(kind)) .count() }; let ring = 2 * (HOME_INTERIOR.x + 2) + 2 * HOME_INTERIOR.y; assert_eq!( count(StructureKind::Wall) + count(StructureKind::Door), ring as usize ); assert_eq!(count(StructureKind::Door), 1); assert_eq!(count(StructureKind::Bed), 2); let door = plan .iter() .find(|(_, k)| **k == BuildKind::Structure(StructureKind::Door)) .unwrap() .0; assert_eq!(door.y, home.lo.y); for bed in home.bed_tiles() { assert!(home.interior().any(|t| t == bed)); } } // Gold has one use: a statue in each finished home, which lifts the family's mood. Planning // it is what makes the colony want gold at all. Finished homes also get a hearth, where the // family cooks (one campfire can't feed a big colony) and which heats the home, and a wooden // floor (a smaller mood boost), built under beds, statue and hearth alike. #[test] fn finished_homes_get_a_statue_and_floor() { let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); let home = Home::at(UVec2::new(5, 5)); let door = UVec2::new(7, 5); for tile in home.ring().filter(|t| *t != door) { map.set_wall(tile, true); } for tile in home.interior() { map.set_roof(tile, true); } let mut app = App::new(); app.insert_resource(map) .init_resource::() .init_resource::() .init_resource::() .insert_resource(Homes { by_family: HashMap::from([(1, home)]), ..default() }) .init_resource::() .init_resource::() .add_systems(Update, plan_homes); app.world_mut().spawn(( Structure { kind: StructureKind::Door, }, TilePos(door), )); app.world_mut() .spawn((Colonist { name: "Ada".into() }, Family(1))); app.update(); let blueprints = app.world().resource::(); assert_eq!( blueprints.get(home.statue_tile()), Some(BuildKind::Structure(StructureKind::Statue)) ); let floors = home .interior() .filter(|t| blueprints.get(*t) == Some(BuildKind::Floor)) .count(); assert_eq!(floors, home.interior().count() - 3); assert_eq!( blueprints.get(home.bed_tiles_for(1)[0]), Some(BuildKind::Structure(StructureKind::Bed)) ); assert_eq!( blueprints.get(home.hearth_tile()), Some(BuildKind::Structure(StructureKind::Hearth)) ); } fn walled(map: &mut WorldMap, home: Home, door: UVec2) -> HashSet { for tile in home.ring().filter(|t| *t != door) { map.set_wall(tile, true); } HashSet::from([door]) } fn reachable(map: &WorldMap, from: UVec2) -> Reach { Reach::flood(map, from) } // Homes reuse walls that are already standing (a shared wall instead of two side by side) and // only plan the missing ones; the door goes on a side that opens onto walkable ground. #[test] fn new_homes_share_existing_walls() { let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass); for y in 0..40 { map.set_wall(UVec2::new(27, y), true); } let mut zones = Zones::default(); zones.add_tiles(&map, [UVec2::new(20, 20)]); let home = plan_on(&map, &zones, &HashSet::new()); assert!(home.lo.x == 27 || home.hi.x == 27, "{home:?}"); let plan = home.blueprints(&map, UVec2::new(20, 20), 2); assert!(plan.keys().all(|t| t.x != 27)); let door = plan .iter() .find(|(_, k)| **k == BuildKind::Structure(StructureKind::Door)) .map(|(t, _)| *t) .unwrap(); assert!(map.move_cost(home.outward(door)).is_some()); } // A home whose only door faces a wall can't be entered; it gets a new door on a side that // opens onto ground connected to the colony, knocking out a wall first if needed. #[test] fn homes_with_a_blocked_door_get_a_new_one() { let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); let home = Home::at(UVec2::new(5, 5)); let blocked = UVec2::new(7, 5); let doors = walled(&mut map, home, blocked); map.set_wall(UVec2::new(7, 4), true); let colony = UVec2::new(7, 0); let open = reachable(&map, colony); let (site, kind) = door_repair(&map, &doors, &open, &home, colony).unwrap(); assert_eq!(kind, BuildKind::Deconstruct); assert!(home.door_sites().any(|t| t == site) && site != blocked); map.set_wall(site, false); let open = reachable(&map, colony); assert_eq!( door_repair(&map, &doors, &open, &home, colony), Some((site, BuildKind::Structure(StructureKind::Door))) ); let fine = walled( &mut WorldMap::filled(UVec2::splat(20), Terrain::Grass), home, blocked, ); let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); assert_eq!( door_repair(&map, &fine, &reachable(&map, colony), &home, colony), None ); } // Blueprints sealed off from the colony can never be built; they only inflate material // targets, so they are dropped (a home's own repairs reopen it and plan again). #[test] fn unreachable_blueprints_are_dropped() { let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); let pocket = Home::at(UVec2::new(12, 12)); for tile in pocket.ring() { map.set_wall(tile, true); } let mut app = App::new(); app.insert_resource(map) .init_resource::() .init_resource::() .init_resource::() .insert_resource(Blueprints(HashMap::from([ (UVec2::new(14, 14), BuildKind::Roof), (UVec2::new(2, 2), BuildKind::Roof), (UVec2::new(12, 12), BuildKind::Deconstruct), ]))) .init_resource::() .init_resource::() .add_systems(Update, plan_homes); app.update(); let blueprints = app.world().resource::(); assert_eq!(blueprints.get(UVec2::new(14, 14)), None); assert!(blueprints.get(UVec2::new(2, 2)).is_some()); assert!(blueprints.get(UVec2::new(12, 12)).is_some()); } // Couples move into the homes of families that died out before anyone builds anew, a solo // colonist plans a small room of their own, and communal beds beyond the adults who still // lack a home are taken down. #[test] fn vacant_homes_are_reused_and_spare_beds_removed() { let mut map = WorldMap::filled(UVec2::splat(30), Terrain::Grass); let old = Home::at(UVec2::new(5, 5)); let door = UVec2::new(7, 5); walled(&mut map, old, door); for tile in old.interior() { map.set_roof(tile, true); } let mut app = App::new(); let map_for_rooms = map.clone(); app.insert_resource(map) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .insert_resource(Rooms::detect(&map_for_rooms, &HashSet::from([door]))) .add_systems(Update, plan_homes); let world = app.world_mut(); let structure = |kind| Structure { kind }; world.spawn((structure(StructureKind::Door), TilePos(door))); for bed in old.bed_tiles() { world.spawn((structure(StructureKind::Bed), TilePos(bed))); } for x in 20..24 { world.spawn((structure(StructureKind::Bed), TilePos(UVec2::new(x, 20)))); } let a = world .spawn((Colonist { name: "Ada".into() }, Family(2))) .id(); let b = world .spawn(( Colonist { name: "Bram".into(), }, Family(2), Partner(a), )) .id(); world.entity_mut(a).insert(Partner(b)); world.spawn(Colonist { name: "Cleo".into(), }); app.update(); let homes = app.world().resource::(); assert_eq!(homes.by_family.get(&2), Some(&old)); let blueprints = app.world().resource::(); let removed = blueprints .0 .values() .filter(|k| **k == BuildKind::Deconstruct) .count(); assert_eq!(removed, 3); let homes = app.world().resource::(); assert_eq!(homes.by_family.len(), 2); let solo = homes.by_family.values().find(|h| **h != old).unwrap(); assert_eq!(solo.interior_size(), desired_interior(1, 0)); } // When the player demolishes a home, the family stops treating it as theirs (and plans a new // one) once most of its walls are gone; otherwise colonists kept putting doors in the gaps. #[test] fn torn_down_homes_are_forgotten() { let map = WorldMap::filled(UVec2::splat(30), Terrain::Grass); let home = Home::at(UVec2::new(5, 5)); let mut standing = map.clone(); for tile in home.ring() { standing.set_wall(tile, true); } let mut ruin = map.clone(); for tile in home.ring().take(4) { ruin.set_wall(tile, true); } let doors = HashSet::new(); assert!(!torn_down(&standing, &doors, &home)); assert!(torn_down(&ruin, &doors, &home)); let mut app = App::new(); app.insert_resource(ruin) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .insert_resource(Homes { by_family: HashMap::from([(1, home)]), ..default() }) .init_resource::() .add_systems(Update, plan_homes); app.world_mut() .spawn((Colonist { name: "Ada".into() }, Family(1))); app.update(); assert!(app.world().resource::().of(Some(Family(1))) != Some(&home)); } // Homes are sized to the household (a solo room, a couple's room, more space per child). // When a family outgrows its home, colonists knock out one side wall and rebuild it further // out, reusing walls that already stand there. #[test] fn growing_families_move_a_wall_out() { assert_eq!(desired_interior(1, 0), UVec2::new(3, 2)); assert_eq!(desired_interior(2, 1), UVec2::new(5, 3)); let mut map = WorldMap::filled(UVec2::splat(30), Terrain::Grass); let home = Home::at(UVec2::new(10, 10)); let door = UVec2::new(12, 10); for tile in home.ring().filter(|t| *t != door) { map.set_wall(tile, true); } for tile in home.interior() { map.set_roof(tile, true); } let mut app = App::new(); app.insert_resource(map.clone()) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .insert_resource(Rooms::detect(&map, &HashSet::from([door]))) .insert_resource(Homes { by_family: HashMap::from([(1, home)]), ..default() }) .add_systems(Update, plan_homes); let world = app.world_mut(); world.spawn(( Structure { kind: StructureKind::Door, }, TilePos(door), )); let a = world .spawn((Colonist { name: "Ada".into() }, Family(1))) .id(); let b = world .spawn(( Colonist { name: "Bram".into(), }, Family(1), Partner(a), )) .id(); world.entity_mut(a).insert(Partner(b)); world.spawn(( Colonist { name: "Cleo".into(), }, Family(1), Child { born: 0 }, )); app.update(); let grown = app.world().resource::().by_family[&1]; assert_eq!(grown.interior_size(), UVec2::new(5, 3)); let blueprints = app.world().resource::(); let opened = home.ring().filter(|t| grown.interior().any(|i| i == *t)); for tile in opened { assert_eq!(blueprints.get(tile), Some(BuildKind::Deconstruct)); } let walls = grown .ring() .filter(|t| !home.contains(*t)) .filter(|t| blueprints.get(*t) == Some(BuildKind::Structure(StructureKind::Wall))) .count(); assert_eq!(walls, 5); } // Colonists keep their busiest routes open: no home (walls or floor) goes on a paved road or // a heavily worn path, so the traffic that wore it in can keep flowing. #[test] fn homes_avoid_busy_paths() { let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Stone); for y in 0..40 { for x in 22..36 { map.set(UVec2::new(x, y), Terrain::Grass); } map.set(UVec2::new(28, y), Terrain::Paved); map.set_wear(UVec2::new(27, y), crate::paths::BUSY_WEAR); } let ctx = HomeContext { rooms: &Rooms::detect(&map, &HashSet::new()), map: &map, zones: &Zones::default(), blueprints: &Blueprints::default(), homes: &Homes::default(), occupied: &HashSet::new(), }; let home = ctx.plan(UVec2::new(24, 20)).unwrap(); assert!(home.lo.x > 28, "{home:?}"); } // Couples form between people who like each other; rivals never pair up however much time // they spend together. #[test] fn rivals_never_become_partners() { let e = |i: u32| Entity::from_raw_u32(i).unwrap(); let single = |i: u32, x: u32| Single { entity: e(i), tile: UVec2::new(x, 0), family: None, }; let pair = [single(1, 0), single(2, 1)]; let mut affinity = Affinity::default(); let mut rng = SimRng::new(1); for _ in 0..BOND_HOURS * 20 { assert!(bond_hour(&mut affinity, &pair, &mut rng, |_, _| -0.6).is_empty()); } } }