src/family.rs 19.7 K · 625 lines · raw · history

1 use std::collections::{HashMap, HashSet};
2
3 use bevy::prelude::*;
4
5 use crate::{
6 building::{Blueprints, BuildKind, enclosed_room},
7 colonist::{Colonist, ColonistAssets, NAMES, spawn_colonist},
8 economy::in_colony,
9 items::{Item, ItemAssets},
10 pathfinding::nearest_tile,
11 population::{ColonyLog, MAX_COLONISTS, food_in_store},
12 rng::SimRng,
13 sim::{SimSystems, SimTick, TICKS_PER_HOUR},
14 structures::{Structure, StructureKind},
15 world::{TilePos, WorldMap},
16 worldgen::WorldGenerated,
17 zones::{Zones, can_zone},
18 };
19
20 pub struct FamilyPlugin;
21
22 impl Plugin for FamilyPlugin {
23 fn build(&self, app: &mut App) {
24 app.init_resource::<Homes>()
25 .init_resource::<Affinity>()
26 .add_systems(Update, clear_families.run_if(on_message::<WorldGenerated>))
27 .add_systems(
28 FixedUpdate,
29 (
30 plan_homes.in_set(SimSystems::Decide),
31 (bond, births, grow_up).chain().in_set(SimSystems::Act),
32 ),
33 );
34 }
35 }
36
37 pub const HOME_INTERIOR: UVec2 = UVec2::new(4, 3);
38 pub const HOME_MIN_DISTANCE: i32 = 6;
39 pub const BOND_RANGE: i32 = 4;
40 pub const BOND_HOURS: u32 = 16;
41 pub const BOND_CHANCE: f32 = 0.2;
42 pub const MAX_CHILDREN: usize = 2;
43 pub const BIRTH_CHANCE: f32 = 0.35;
44 pub const CHILDHOOD_TICKS: u64 = 4 * 24 * TICKS_PER_HOUR;
45 pub const CHILD_SCALE: f32 = 0.6;
46 const BIRTH_HOUR_TICK: u64 = 20 * TICKS_PER_HOUR;
47 const DAY_TICKS: u64 = 24 * TICKS_PER_HOUR;
48 const HOME_PLAN_INTERVAL_TICKS: u64 = 300;
49 const HOME_SEARCH_TILES: usize = 6000;
50 const HOME_CLEARANCE: i32 = 1;
51
52 #[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Hash)]
53 pub struct Family(pub u32);
54
55 #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
56 pub struct Partner(pub Entity);
57
58 #[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
59 pub struct Child {
60 pub born: u64,
61 }
62
63 impl Child {
64 pub fn days_left(&self, tick: SimTick) -> u64 {
65 (self.born + CHILDHOOD_TICKS)
66 .saturating_sub(tick.0)
67 .div_ceil(DAY_TICKS)
68 }
69 }
70
71 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
72 pub struct Home {
73 pub lo: UVec2,
74 pub hi: UVec2,
75 }
76
77 impl Home {
78 pub fn at(lo: UVec2) -> Self {
79 Self {
80 lo,
81 hi: lo + HOME_INTERIOR + UVec2::ONE,
82 }
83 }
84
85 pub fn contains(&self, tile: UVec2) -> bool {
86 tile.cmpge(self.lo).all() && tile.cmple(self.hi).all()
87 }
88
89 pub fn interior(&self) -> impl Iterator<Item = UVec2> + use<> {
90 let (lo, hi) = (self.lo + UVec2::ONE, self.hi - UVec2::ONE);
91 (lo.y..=hi.y).flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
92 }
93
94 pub fn ring(&self) -> impl Iterator<Item = UVec2> + use<> {
95 let (lo, hi) = (self.lo, self.hi);
96 (lo.y..=hi.y)
97 .flat_map(move |y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
98 .filter(move |t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y)
99 }
100
101 pub fn bed_tiles(&self) -> [UVec2; 2] {
102 let corner = UVec2::new(self.lo.x + 1, self.hi.y - 1);
103 [corner, corner + UVec2::X]
104 }
105
106 pub fn blueprints(&self, toward: UVec2) -> HashMap<UVec2, BuildKind> {
107 let corner = |t: &UVec2| {
108 (t.x == self.lo.x || t.x == self.hi.x) && (t.y == self.lo.y || t.y == self.hi.y)
109 };
110 let door = self
111 .ring()
112 .filter(|t| !corner(t))
113 .min_by(|a, b| {
114 a.as_vec2()
115 .distance_squared(toward.as_vec2())
116 .total_cmp(&b.as_vec2().distance_squared(toward.as_vec2()))
117 .then((a.y, a.x).cmp(&(b.y, b.x)))
118 })
119 .unwrap_or(self.lo);
120 let mut plan: HashMap<UVec2, BuildKind> = self
121 .ring()
122 .map(|t| (t, BuildKind::Structure(StructureKind::Wall)))
123 .collect();
124 plan.insert(door, BuildKind::Structure(StructureKind::Door));
125 for bed in self.bed_tiles() {
126 plan.insert(bed, BuildKind::Structure(StructureKind::Bed));
127 }
128 plan
129 }
130 }
131
132 #[derive(Resource, Default, Debug)]
133 pub struct Homes {
134 pub by_family: HashMap<u32, Home>,
135 next_family: u32,
136 }
137
138 impl Homes {
139 pub fn new_family(&mut self) -> Family {
140 self.next_family += 1;
141 Family(self.next_family)
142 }
143
144 pub fn owner(&self, tile: UVec2) -> Option<u32> {
145 self.by_family
146 .iter()
147 .find(|(_, home)| home.contains(tile))
148 .map(|(family, _)| *family)
149 }
150
151 pub fn of(&self, family: Option<Family>) -> Option<&Home> {
152 self.by_family.get(&family?.0)
153 }
154 }
155
156 #[derive(Resource, Default, Debug)]
157 pub struct Affinity(pub HashMap<(Entity, Entity), u32>);
158
159 fn clear_families(mut homes: ResMut<Homes>, mut affinity: ResMut<Affinity>) {
160 homes.by_family.clear();
161 affinity.0.clear();
162 }
163
164 #[derive(Clone, Copy)]
165 pub struct Single {
166 pub entity: Entity,
167 pub tile: UVec2,
168 pub family: Option<Family>,
169 }
170
171 pub fn bond_hour(
172 affinity: &mut Affinity,
173 singles: &[Single],
174 rng: &mut SimRng,
175 ) -> Vec<(Entity, Entity)> {
176 let mut bonded: Vec<(Entity, Entity)> = Vec::new();
177 let taken =
178 |bonded: &[(Entity, Entity)], e: Entity| bonded.iter().any(|&(a, b)| a == e || b == e);
179 for (i, a) in singles.iter().enumerate() {
180 for b in &singles[i + 1..] {
181 let near = (a.tile.as_ivec2() - b.tile.as_ivec2()).abs().max_element() <= BOND_RANGE;
182 let kin = a.family.is_some() && a.family == b.family;
183 if !near || kin {
184 continue;
185 }
186 let key = (a.entity.min(b.entity), a.entity.max(b.entity));
187 let hours = affinity.0.entry(key).or_insert(0);
188 *hours += 1;
189 if *hours >= BOND_HOURS
190 && !taken(&bonded, a.entity)
191 && !taken(&bonded, b.entity)
192 && rng.range_f32(0.0..1.0) < BOND_CHANCE
193 {
194 bonded.push(key);
195 }
196 }
197 }
198 for pair in &bonded {
199 affinity
200 .0
201 .retain(|&(a, b), _| ![a, b].iter().any(|e| *e == pair.0 || *e == pair.1));
202 }
203 bonded
204 }
205
206 #[allow(clippy::type_complexity)]
207 fn bond(
208 mut commands: Commands,
209 tick: Res<SimTick>,
210 mut rng: ResMut<SimRng>,
211 mut homes: ResMut<Homes>,
212 mut affinity: ResMut<Affinity>,
213 mut log: ResMut<ColonyLog>,
214 colonists: Query<(
215 Entity,
216 &Colonist,
217 &TilePos,
218 Option<&Family>,
219 Option<&Partner>,
220 Has<Child>,
221 )>,
222 ) {
223 if !tick.0.is_multiple_of(TICKS_PER_HOUR) {
224 return;
225 }
226 for (entity, _, _, _, partner, _) in &colonists {
227 if let Some(partner) = partner
228 && !colonists.contains(partner.0)
229 {
230 commands.entity(entity).remove::<Partner>();
231 }
232 }
233 let singles: Vec<Single> = colonists
234 .iter()
235 .filter(|(.., partner, child)| partner.is_none() && !child)
236 .map(|(entity, _, tile, family, ..)| Single {
237 entity,
238 tile: tile.0,
239 family: family.copied(),
240 })
241 .collect();
242 for (a, b) in bond_hour(&mut affinity, &singles, &mut rng) {
243 let family = homes.new_family();
244 commands.entity(a).insert((Partner(b), family));
245 commands.entity(b).insert((Partner(a), family));
246 let name = |e: Entity| {
247 colonists
248 .get(e)
249 .map(|c| c.1.name.clone())
250 .unwrap_or_default()
251 };
252 log.push(
253 *tick,
254 format!("{} and {} became partners", name(a), name(b)),
255 );
256 }
257 }
258
259 pub struct HomeContext<'a> {
260 pub map: &'a WorldMap,
261 pub zones: &'a Zones,
262 pub blueprints: &'a Blueprints,
263 pub homes: &'a Homes,
264 pub occupied: &'a HashSet<UVec2>,
265 }
266
267 impl HomeContext<'_> {
268 fn claimed(&self, tile: UVec2) -> bool {
269 self.zones.is_stockpile(tile)
270 || self.zones.is_farm(tile)
271 || self.blueprints.get(tile).is_some()
272 || self.homes.owner(tile).is_some()
273 }
274
275 fn open(&self, tile: IVec2) -> bool {
276 self.map.in_bounds(tile)
277 && can_zone(self.map, tile)
278 && self.map.plant(tile).is_none()
279 && !self.map.is_wall(tile)
280 && !self.occupied.contains(&tile.as_uvec2())
281 && !self.claimed(tile.as_uvec2())
282 }
283
284 fn fits(&self, home: Home) -> bool {
285 let (lo, hi) = (home.lo.as_ivec2(), home.hi.as_ivec2());
286 let (outer_lo, outer_hi) = (lo - HOME_CLEARANCE, hi + HOME_CLEARANCE);
287 (outer_lo.y..=outer_hi.y).all(|y| {
288 (outer_lo.x..=outer_hi.x).all(|x| {
289 let tile = IVec2::new(x, y);
290 let core = tile.cmpge(lo).all() && tile.cmple(hi).all();
291 if core {
292 self.open(tile)
293 } else {
294 !self.map.in_bounds(tile) || !self.claimed(tile.as_uvec2())
295 }
296 })
297 })
298 }
299
300 pub fn plan(&self, colony_home: UVec2) -> Option<Home> {
301 let center = (HOME_INTERIOR / 2 + UVec2::ONE).as_ivec2();
302 let mut found = None;
303 nearest_tile(self.map, colony_home, HOME_SEARCH_TILES, |p| {
304 let lo = p.as_ivec2() - center;
305 if lo.min_element() < 0 {
306 return false;
307 }
308 let home = Home::at(lo.as_uvec2());
309 let middle = (home.lo + home.hi).as_ivec2() / 2;
310 if (middle - colony_home.as_ivec2()).abs().max_element() < HOME_MIN_DISTANCE {
311 return false;
312 }
313 found = self.fits(home).then_some(home);
314 found.is_some()
315 })?;
316 found
317 }
318 }
319
320 pub fn roofs_needed(map: &WorldMap, doors: &HashSet<UVec2>, home: &Home) -> Vec<UVec2> {
321 let interior: Vec<UVec2> = home.interior().collect();
322 let Some(room) = enclosed_room(map, doors, interior[0]) else {
323 return Vec::new();
324 };
325 if room.len() != interior.len() {
326 return Vec::new();
327 }
328 interior
329 .into_iter()
330 .filter(|t| !map.is_roofed(t.as_ivec2()))
331 .collect()
332 }
333
334 #[allow(clippy::too_many_arguments)]
335 fn plan_homes(
336 tick: Res<SimTick>,
337 map: Res<WorldMap>,
338 zones: Res<Zones>,
339 mut homes: ResMut<Homes>,
340 mut blueprints: ResMut<Blueprints>,
341 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
342 items: Query<&TilePos, With<Item>>,
343 families: Query<(&Family, Has<Partner>), With<Colonist>>,
344 ) {
345 if !tick.0.is_multiple_of(HOME_PLAN_INTERVAL_TICKS) {
346 return;
347 }
348 let living: HashSet<u32> = families.iter().map(|(f, _)| f.0).collect();
349 homes.by_family.retain(|family, _| living.contains(family));
350 let doors: HashSet<UVec2> = structures
351 .iter()
352 .filter(|(_, s)| s.kind == StructureKind::Door)
353 .map(|(pos, _)| pos.0)
354 .collect();
355 for home in homes.by_family.values() {
356 if blueprints.0.keys().any(|t| home.contains(*t)) {
357 continue;
358 }
359 for tile in roofs_needed(&map, &doors, home) {
360 blueprints.0.insert(tile, BuildKind::Roof);
361 }
362 }
363 let Some(family) = families
364 .iter()
365 .filter(|(f, partnered)| *partnered && !homes.by_family.contains_key(&f.0))
366 .map(|(f, _)| f.0)
367 .min()
368 else {
369 return;
370 };
371 let occupied: HashSet<UVec2> = structures
372 .iter()
373 .map(|(pos, _)| pos.0)
374 .chain(items.iter().map(|pos| pos.0))
375 .collect();
376 let colony_home = zones.home(map.size / 2);
377 let ctx = HomeContext {
378 map: &map,
379 zones: &zones,
380 blueprints: &blueprints,
381 homes: &homes,
382 occupied: &occupied,
383 };
384 if let Some(home) = ctx.plan(colony_home) {
385 blueprints.0.extend(home.blueprints(colony_home));
386 homes.by_family.insert(family, home);
387 }
388 }
389
390 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
391 fn births(
392 mut commands: Commands,
393 tick: Res<SimTick>,
394 map: Res<WorldMap>,
395 zones: Res<Zones>,
396 homes: Res<Homes>,
397 assets: Res<ColonistAssets>,
398 item_assets: Res<ItemAssets>,
399 mut materials: ResMut<Assets<ColorMaterial>>,
400 mut rng: ResMut<SimRng>,
401 mut log: ResMut<ColonyLog>,
402 items: Query<(&Item, &TilePos)>,
403 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
404 colonists: Query<(&Colonist, Option<&Family>, Has<Partner>, Has<Child>)>,
405 ) {
406 if tick.0 % DAY_TICKS != BIRTH_HOUR_TICK {
407 return;
408 }
409 let count = colonists.iter().count();
410 let colony_home = zones.home(map.size / 2);
411 let food = food_in_store(
412 items
413 .iter()
414 .filter(|(_, pos)| in_colony(colony_home, pos.0))
415 .map(|(item, _)| item),
416 );
417 let mut born = 0;
418 let mut families: Vec<(&u32, &Home)> = homes.by_family.iter().collect();
419 families.sort_by_key(|(family, _)| **family);
420 for (&family, home) in families {
421 let members = || {
422 colonists
423 .iter()
424 .filter(move |c| c.1 == Some(&Family(family)))
425 };
426 let parents: Vec<&str> = members()
427 .filter(|c| c.2)
428 .map(|c| c.0.name.as_str())
429 .collect();
430 let children = members().filter(|c| c.3).count();
431 let beds = structures
432 .iter()
433 .filter(|(pos, s)| s.kind == StructureKind::Bed && home.contains(pos.0))
434 .count();
435 let built = home.interior().all(|t| map.is_roofed(t.as_ivec2()));
436 let room = count + born < MAX_COLONISTS && food >= (count + born) as f32;
437 if parents.len() < 2 || children >= MAX_CHILDREN || beds < 2 || !built || !room {
438 continue;
439 }
440 if rng.range_f32(0.0..1.0) >= BIRTH_CHANCE {
441 continue;
442 }
443 let taken: Vec<&str> = colonists.iter().map(|c| c.0.name.as_str()).collect();
444 let index = (0..NAMES.len())
445 .find(|&i| !taken.contains(&NAMES[i]))
446 .unwrap_or(count);
447 let tile = home
448 .interior()
449 .find(|t| !home.bed_tiles().contains(t))
450 .unwrap_or(home.lo);
451 let child = spawn_colonist(
452 &mut commands,
453 &assets,
454 &item_assets,
455 &mut materials,
456 index,
457 tile,
458 );
459 commands.entity(child).insert((
460 Child { born: tick.0 },
461 Family(family),
462 Transform::from_translation(WorldMap::tile_center(tile).extend(1.0))
463 .with_scale(Vec3::splat(CHILD_SCALE)),
464 ));
465 born += 1;
466 log.push(
467 *tick,
468 format!(
469 "{} and {} had a child, {}",
470 parents[0],
471 parents[1],
472 NAMES[index % NAMES.len()]
473 ),
474 );
475 }
476 }
477
478 fn grow_up(
479 mut commands: Commands,
480 tick: Res<SimTick>,
481 mut log: ResMut<ColonyLog>,
482 mut children: Query<(Entity, &Colonist, &Child, &mut Transform)>,
483 ) {
484 if !tick.0.is_multiple_of(TICKS_PER_HOUR) {
485 return;
486 }
487 for (entity, colonist, child, mut transform) in &mut children {
488 if tick.0 < child.born + CHILDHOOD_TICKS {
489 continue;
490 }
491 transform.scale = Vec3::ONE;
492 commands.entity(entity).remove::<Child>();
493 log.push(*tick, format!("{} has grown up", colonist.name));
494 }
495 }
496
497 #[cfg(test)]
498 mod tests {
499 use super::*;
500 use crate::world::Terrain;
501
502 fn singles(app_world: &mut World, tiles: &[UVec2]) -> Vec<Single> {
503 tiles
504 .iter()
505 .map(|&tile| Single {
506 entity: app_world.spawn_empty().id(),
507 tile,
508 family: None,
509 })
510 .collect()
511 }
512
513 fn bond_for(hours: u32, singles: &[Single]) -> Vec<(Entity, Entity)> {
514 let mut affinity = Affinity::default();
515 let mut rng = SimRng::new(3);
516 let mut bonded: Vec<(Entity, Entity)> = Vec::new();
517 for _ in 0..hours {
518 let free: Vec<Single> = singles
519 .iter()
520 .filter(|s| !bonded.iter().any(|&(a, b)| a == s.entity || b == s.entity))
521 .copied()
522 .collect();
523 bonded.extend(bond_hour(&mut affinity, &free, &mut rng));
524 }
525 bonded
526 }
527
528 #[test]
529 fn colonists_who_spend_time_together_become_partners() {
530 let mut world = World::new();
531 let near = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(6, 7)]);
532 assert!(bond_for(BOND_HOURS - 1, &near).is_empty());
533 assert_eq!(bond_for(BOND_HOURS * 4, &near).len(), 1);
534 let apart = singles(&mut world, &[UVec2::new(5, 5), UVec2::new(15, 5)]);
535 assert!(bond_for(BOND_HOURS * 4, &apart).is_empty());
536 }
537
538 #[test]
539 fn everyone_has_at_most_one_partner_and_family_never_pairs() {
540 let mut world = World::new();
541 let mut trio = singles(&mut world, &[UVec2::ONE, UVec2::ONE, UVec2::ONE]);
542 let bonded = bond_for(BOND_HOURS * 10, &trio);
543 assert_eq!(bonded.len(), 1);
544 for single in &mut trio {
545 single.family = Some(Family(1));
546 }
547 assert!(bond_for(BOND_HOURS * 10, &trio).is_empty());
548 }
549
550 fn plan_on(map: &WorldMap, zones: &Zones, occupied: &HashSet<UVec2>) -> Home {
551 HomeContext {
552 map,
553 zones,
554 blueprints: &Blueprints::default(),
555 homes: &Homes::default(),
556 occupied,
557 }
558 .plan(UVec2::new(20, 20))
559 .unwrap()
560 }
561
562 #[test]
563 fn homes_go_on_open_ground_away_from_the_colony_center() {
564 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass);
565 for x in 0..40 {
566 map.set_plant(UVec2::new(x, 14), Some(crate::world::Plant::Tree));
567 }
568 let mut zones = Zones::default();
569 zones.add_tiles(&map, [UVec2::new(20, 20), UVec2::new(21, 20)]);
570 let occupied = HashSet::from([UVec2::new(20, 26)]);
571 let home = plan_on(&map, &zones, &occupied);
572 let center = (home.lo + home.hi).as_ivec2() / 2;
573 assert!((center - IVec2::new(20, 20)).abs().max_element() >= HOME_MIN_DISTANCE);
574 for y in home.lo.y - 1..=home.hi.y + 1 {
575 for x in home.lo.x - 1..=home.hi.x + 1 {
576 let tile = UVec2::new(x, y);
577 assert!(!zones.is_stockpile(tile));
578 if home.contains(tile) {
579 assert!(map.plant(tile.as_ivec2()).is_none());
580 assert!(!occupied.contains(&tile));
581 }
582 }
583 }
584 }
585
586 #[test]
587 fn a_home_is_a_walled_room_with_a_door_and_two_beds() {
588 let home = Home::at(UVec2::new(10, 10));
589 let plan = home.blueprints(UVec2::new(12, 0));
590 let count = |kind: StructureKind| {
591 plan.values()
592 .filter(|k| **k == BuildKind::Structure(kind))
593 .count()
594 };
595 let ring = 2 * (HOME_INTERIOR.x + 2) + 2 * HOME_INTERIOR.y;
596 assert_eq!(
597 count(StructureKind::Wall) + count(StructureKind::Door),
598 ring as usize
599 );
600 assert_eq!(count(StructureKind::Door), 1);
601 assert_eq!(count(StructureKind::Bed), 2);
602 let door = plan
603 .iter()
604 .find(|(_, k)| **k == BuildKind::Structure(StructureKind::Door))
605 .unwrap()
606 .0;
607 assert_eq!(door.y, home.lo.y);
608 for bed in home.bed_tiles() {
609 assert!(home.interior().any(|t| t == bed));
610 }
611 }
612
613 #[test]
614 fn finished_homes_get_a_roof() {
615 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
616 let home = Home::at(UVec2::new(5, 5));
617 let door = UVec2::new(7, 5);
618 for tile in home.ring().filter(|t| *t != door) {
619 map.set_wall(tile, true);
620 }
621 assert!(roofs_needed(&map, &HashSet::new(), &home).is_empty());
622 let roofs = roofs_needed(&map, &HashSet::from([door]), &home);
623 assert_eq!(roofs.len(), (HOME_INTERIOR.x * HOME_INTERIOR.y) as usize);
624 }
625 }