| @@ -0,0 +1,625 @@ |
|
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 |
+} |