src/building.rs 19.3 K · 603 lines · raw · history

1 use std::collections::{HashMap, HashSet, VecDeque};
2
3 use bevy::prelude::*;
4
5 use crate::{
6 actions::Action,
7 colonist::Colonist,
8 family::Homes,
9 items::{Item, ItemKind},
10 selection::{Cursor, update_cursor},
11 sim::{SimSystems, SimTick},
12 structures::{Structure, StructureKind},
13 ui::PointerOverUi,
14 world::{TILE_SIZE, TilePos, WorldMap},
15 worldgen::WorldGenerated,
16 zones::{PREVIEW_ADD_COLOR, PREVIEW_REMOVE_COLOR, Zones, can_zone, removing},
17 };
18
19 pub struct BuildingPlugin;
20
21 impl Plugin for BuildingPlugin {
22 fn build(&self, app: &mut App) {
23 app.init_resource::<Blueprints>()
24 .add_systems(
25 Update,
26 (
27 clear_blueprints.run_if(on_message::<WorldGenerated>),
28 draw_blueprints,
29 ),
30 )
31 .init_resource::<BuildTool>()
32 .add_systems(
33 PostUpdate,
34 (build_tool, draw_build_preview)
35 .chain()
36 .after(update_cursor),
37 )
38 .add_systems(FixedUpdate, plan_shelter.in_set(SimSystems::Decide));
39 }
40 }
41
42 pub const MAX_ROOM_TILES: usize = 120;
43 const PLAN_INTERVAL_TICKS: u64 = 300;
44 const MAX_ROOM_MARGIN: i32 = 2;
45 const CRAMPED_SIDE_COST: i32 = 3;
46 const WALL_BLUEPRINT_COLOR: Color = Color::srgba(0.5, 0.75, 1.0, 0.8);
47 const DOOR_BLUEPRINT_COLOR: Color = Color::srgba(1.0, 0.8, 0.4, 0.8);
48 const PAVE_BLUEPRINT_COLOR: Color = Color::srgba(0.85, 0.85, 0.85, 0.5);
49 const ROOF_BLUEPRINT_COLOR: Color = Color::srgba(0.8, 0.6, 1.0, 0.35);
50
51 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
52 pub enum BuildKind {
53 Structure(StructureKind),
54 Roof,
55 Pave,
56 Refuel(StructureKind),
57 }
58
59 impl BuildKind {
60 pub fn cost(self) -> (ItemKind, u32) {
61 match self {
62 BuildKind::Structure(kind) => kind.cost(),
63 BuildKind::Roof => (ItemKind::Wood, 1),
64 BuildKind::Pave => (ItemKind::Stone, 1),
65 BuildKind::Refuel(kind) => kind.cost(),
66 }
67 }
68
69 pub fn work(self) -> u32 {
70 match self {
71 BuildKind::Structure(StructureKind::Campfire) => 360,
72 BuildKind::Structure(StructureKind::Torch) => 120,
73 BuildKind::Structure(StructureKind::Bed) => 600,
74 BuildKind::Structure(StructureKind::Wall) => 300,
75 BuildKind::Structure(StructureKind::Door) => 300,
76 BuildKind::Roof => 120,
77 BuildKind::Pave => 120,
78 BuildKind::Refuel(_) => 60,
79 }
80 }
81
82 pub fn name(self) -> &'static str {
83 match self {
84 BuildKind::Structure(kind) => kind.name(),
85 BuildKind::Roof => "roof",
86 BuildKind::Pave => "paving",
87 BuildKind::Refuel(_) => "refuelling",
88 }
89 }
90
91 pub fn blocks_tile(self) -> bool {
92 self == BuildKind::Structure(StructureKind::Wall)
93 }
94 }
95
96 #[derive(Resource, Default, Debug, Clone)]
97 pub struct Blueprints(pub HashMap<UVec2, BuildKind>);
98
99 impl Blueprints {
100 pub fn get(&self, tile: UVec2) -> Option<BuildKind> {
101 self.0.get(&tile).copied()
102 }
103
104 pub fn tiles(&self) -> HashSet<UVec2> {
105 self.0.keys().copied().collect()
106 }
107 }
108
109 fn clear_blueprints(mut blueprints: ResMut<Blueprints>) {
110 blueprints.0.clear();
111 }
112
113 pub fn enclosed_room(map: &WorldMap, doors: &HashSet<UVec2>, start: UVec2) -> Option<Vec<UVec2>> {
114 let mut seen = HashSet::from([start]);
115 let mut queue = VecDeque::from([start]);
116 while let Some(tile) = queue.pop_front() {
117 if seen.len() > MAX_ROOM_TILES {
118 return None;
119 }
120 for step in [IVec2::X, IVec2::NEG_X, IVec2::Y, IVec2::NEG_Y] {
121 let next = tile.as_ivec2() + step;
122 if !map.in_bounds(next) {
123 return None;
124 }
125 let next = next.as_uvec2();
126 let barrier = map.is_wall(next.as_ivec2())
127 || doors.contains(&next)
128 || map.get(next.as_ivec2()).is_some_and(|t| t.is_rock());
129 if !barrier && seen.insert(next) {
130 queue.push_back(next);
131 }
132 }
133 }
134 let mut room: Vec<UVec2> = seen.into_iter().collect();
135 room.sort_by_key(|t| (t.y, t.x));
136 Some(room)
137 }
138
139 pub struct ShelterContext<'a> {
140 pub map: &'a WorldMap,
141 pub structures: &'a [(UVec2, StructureKind)],
142 pub items: &'a [UVec2],
143 }
144
145 impl ShelterContext<'_> {
146 fn doors(&self) -> HashSet<UVec2> {
147 self.structures
148 .iter()
149 .filter(|(_, kind)| *kind == StructureKind::Door)
150 .map(|(pos, _)| *pos)
151 .collect()
152 }
153
154 fn wall_buildable(&self, tile: IVec2) -> bool {
155 can_zone(self.map, tile)
156 && self.map.plant(tile).is_none()
157 && !self.items.contains(&tile.as_uvec2())
158 && !self
159 .structures
160 .iter()
161 .any(|(pos, _)| *pos == tile.as_uvec2())
162 }
163
164 fn already_closed(&self, tile: IVec2, doors: &HashSet<UVec2>) -> bool {
165 self.map.is_wall(tile)
166 || doors.contains(&tile.as_uvec2())
167 || self.map.get(tile).is_some_and(|t| t.is_rock())
168 }
169
170 pub fn plan(&self, beds: &[UVec2], toward: UVec2) -> Option<HashMap<UVec2, BuildKind>> {
171 let doors = self.doors();
172 if let Some(&first) = beds.first()
173 && let Some(room) = enclosed_room(self.map, &doors, first)
174 {
175 let roofs: HashMap<UVec2, BuildKind> = room
176 .into_iter()
177 .filter(|t| !self.map.is_roofed(t.as_ivec2()))
178 .map(|t| (t, BuildKind::Roof))
179 .collect();
180 return (!roofs.is_empty()).then_some(roofs);
181 }
182 let min = beds.iter().copied().reduce(|a, b| a.min(b))?.as_ivec2();
183 let max = beds.iter().copied().reduce(|a, b| a.max(b))?.as_ivec2();
184 let margins = 0..=MAX_ROOM_MARGIN;
185 let shapes = margins.clone().flat_map(|l| {
186 let margins = margins.clone();
187 margins.clone().flat_map(move |r| {
188 let margins = margins.clone();
189 margins
190 .clone()
191 .flat_map(move |d| margins.clone().map(move |u| IVec4::new(l, r, d, u)))
192 })
193 });
194 shapes
195 .filter_map(|m| {
196 let lo = min - IVec2::new(m.x, m.z) - 1;
197 let hi = max + IVec2::new(m.y, m.w) + 1;
198 let plan = self.walls_around(lo, hi, toward, &doors)?;
199 let cramped = m.to_array().iter().filter(|&&side| side == 0).count() as i32;
200 let cost = plan.len() as i32 + CRAMPED_SIDE_COST * cramped;
201 Some((cost, m.to_array(), plan))
202 })
203 .min_by_key(|(cost, margins, _)| (*cost, *margins))
204 .map(|(_, _, plan)| plan)
205 }
206
207 fn walls_around(
208 &self,
209 lo: IVec2,
210 hi: IVec2,
211 toward: UVec2,
212 doors: &HashSet<UVec2>,
213 ) -> Option<HashMap<UVec2, BuildKind>> {
214 let ring: Vec<IVec2> = (lo.y..=hi.y)
215 .flat_map(|y| (lo.x..=hi.x).map(move |x| IVec2::new(x, y)))
216 .filter(|t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y)
217 .collect();
218 let mut plan = HashMap::new();
219 for &tile in &ring {
220 if !self.map.in_bounds(tile) {
221 return None;
222 }
223 if self.already_closed(tile, doors) {
224 continue;
225 }
226 if !self.wall_buildable(tile) {
227 return None;
228 }
229 plan.insert(tile.as_uvec2(), BuildKind::Structure(StructureKind::Wall));
230 }
231 let has_door = ring.iter().any(|t| doors.contains(&t.as_uvec2()));
232 if !has_door {
233 let corner = |t: &UVec2| {
234 let t = t.as_ivec2();
235 (t.x == lo.x || t.x == hi.x) && (t.y == lo.y || t.y == hi.y)
236 };
237 let door = plan.keys().filter(|t| !corner(t)).copied().min_by(|a, b| {
238 a.as_vec2()
239 .distance_squared(toward.as_vec2())
240 .total_cmp(&b.as_vec2().distance_squared(toward.as_vec2()))
241 .then((a.y, a.x).cmp(&(b.y, b.x)))
242 })?;
243 plan.insert(door, BuildKind::Structure(StructureKind::Door));
244 }
245 Some(plan)
246 }
247 }
248
249 pub fn plan_shelter(
250 tick: Res<SimTick>,
251 map: Res<WorldMap>,
252 mut zones: ResMut<Zones>,
253 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
254 items: Query<&TilePos, With<Item>>,
255 mut blueprints: ResMut<Blueprints>,
256 homes: Res<Homes>,
257 ) {
258 let shelter_pending = blueprints.0.values().any(|k| *k != BuildKind::Pave);
259 if !tick.0.is_multiple_of(PLAN_INTERVAL_TICKS) || shelter_pending {
260 return;
261 }
262 let structures: Vec<(UVec2, StructureKind)> =
263 structures.iter().map(|(pos, s)| (pos.0, s.kind)).collect();
264 let beds: Vec<UVec2> = structures
265 .iter()
266 .filter(|(pos, kind)| {
267 *kind == StructureKind::Bed
268 && !map.is_roofed(pos.as_ivec2())
269 && homes.owner(*pos).is_none()
270 })
271 .map(|(pos, _)| *pos)
272 .collect();
273 if beds.is_empty() {
274 return;
275 }
276 let items: Vec<UVec2> = items.iter().map(|pos| pos.0).collect();
277 let ctx = ShelterContext {
278 map: &map,
279 structures: &structures,
280 items: &items,
281 };
282 let toward = structures
283 .iter()
284 .find(|(_, kind)| *kind == StructureKind::Campfire)
285 .map(|(pos, _)| *pos)
286 .unwrap_or_else(|| zones.home(beds[0]));
287 if let Some(plan) = ctx.plan(&beds, toward) {
288 for (tile, kind) in &plan {
289 if *kind != BuildKind::Roof {
290 zones.remove_tile(*tile);
291 }
292 }
293 blueprints.0.extend(plan);
294 }
295 }
296
297 pub const BUILD_TOOL_CYCLE: [StructureKind; 5] = [
298 StructureKind::Wall,
299 StructureKind::Door,
300 StructureKind::Campfire,
301 StructureKind::Torch,
302 StructureKind::Bed,
303 ];
304
305 #[derive(Resource, Default, Debug)]
306 pub struct BuildTool {
307 pub kind: Option<StructureKind>,
308 drag_start: Option<UVec2>,
309 }
310
311 impl BuildTool {
312 pub fn active(&self) -> bool {
313 self.kind.is_some()
314 }
315
316 fn cycle(&mut self) {
317 let next = match self.kind {
318 None => Some(0),
319 Some(kind) => BUILD_TOOL_CYCLE
320 .iter()
321 .position(|k| *k == kind)
322 .map(|i| i + 1)
323 .filter(|&i| i < BUILD_TOOL_CYCLE.len()),
324 };
325 self.kind = next.map(|i| BUILD_TOOL_CYCLE[i]);
326 }
327 }
328
329 pub fn placements(kind: StructureKind, start: UVec2, end: UVec2) -> Vec<UVec2> {
330 if kind != StructureKind::Wall {
331 return vec![end];
332 }
333 let (lo, hi) = (start.min(end), start.max(end));
334 (lo.y..=hi.y)
335 .flat_map(|y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
336 .filter(|t| t.x == lo.x || t.x == hi.x || t.y == lo.y || t.y == hi.y)
337 .collect()
338 }
339
340 pub fn place_blueprints(
341 map: &WorldMap,
342 zones: &mut Zones,
343 blueprints: &mut Blueprints,
344 occupied: &HashSet<UVec2>,
345 kind: StructureKind,
346 tiles: &[UVec2],
347 ) {
348 for &tile in tiles {
349 let free = can_zone(map, tile.as_ivec2())
350 && map.plant(tile.as_ivec2()).is_none()
351 && !map.is_wall(tile.as_ivec2())
352 && !occupied.contains(&tile);
353 if !free {
354 continue;
355 }
356 if matches!(kind, StructureKind::Wall | StructureKind::Door) {
357 zones.remove_tile(tile);
358 }
359 blueprints.0.insert(tile, BuildKind::Structure(kind));
360 }
361 }
362
363 #[allow(clippy::too_many_arguments)]
364 fn build_tool(
365 mut actions: MessageReader<Action>,
366 keys: Res<ButtonInput<KeyCode>>,
367 buttons: Res<ButtonInput<MouseButton>>,
368 over_ui: Res<PointerOverUi>,
369 cursor: Res<Cursor>,
370 map: Res<WorldMap>,
371 mut tool: ResMut<BuildTool>,
372 mut zones: ResMut<Zones>,
373 mut blueprints: ResMut<Blueprints>,
374 structures: Query<&TilePos, (With<Structure>, Without<Colonist>)>,
375 ) {
376 for action in actions.read() {
377 match action {
378 Action::CycleBuildTool => tool.cycle(),
379 Action::Cancel
380 | Action::ToggleStockpileTool
381 | Action::ToggleFarmTool
382 | Action::ToggleDesignateTool => tool.kind = None,
383 _ => continue,
384 }
385 tool.drag_start = None;
386 return;
387 }
388 let Some(kind) = tool.kind else {
389 return;
390 };
391 if buttons.just_pressed(MouseButton::Left) && !over_ui.0 {
392 tool.drag_start = cursor.tile;
393 }
394 if !buttons.just_released(MouseButton::Left) {
395 return;
396 }
397 let (Some(start), Some(end)) = (tool.drag_start.take(), cursor.tile) else {
398 return;
399 };
400 if removing(&keys) {
401 let (lo, hi) = (start.min(end), start.max(end));
402 blueprints
403 .0
404 .retain(|t, _| !(t.cmpge(lo).all() && t.cmple(hi).all()));
405 return;
406 }
407 let occupied: HashSet<UVec2> = structures.iter().map(|p| p.0).collect();
408 let tiles = placements(kind, start, end);
409 place_blueprints(&map, &mut zones, &mut blueprints, &occupied, kind, &tiles);
410 }
411
412 fn draw_build_preview(
413 keys: Res<ButtonInput<KeyCode>>,
414 cursor: Res<Cursor>,
415 tool: Res<BuildTool>,
416 mut gizmos: Gizmos,
417 ) {
418 let (Some(kind), Some(end)) = (tool.kind, cursor.tile) else {
419 return;
420 };
421 let start = tool.drag_start.unwrap_or(end);
422 let color = if removing(&keys) {
423 PREVIEW_REMOVE_COLOR
424 } else {
425 PREVIEW_ADD_COLOR
426 };
427 let tiles = if removing(&keys) {
428 let (lo, hi) = (start.min(end), start.max(end));
429 (lo.y..=hi.y)
430 .flat_map(|y| (lo.x..=hi.x).map(move |x| UVec2::new(x, y)))
431 .collect()
432 } else {
433 placements(kind, start, end)
434 };
435 for tile in tiles {
436 gizmos.rect_2d(
437 WorldMap::tile_center(tile),
438 Vec2::splat(TILE_SIZE * 0.7),
439 color,
440 );
441 }
442 }
443
444 fn draw_blueprints(mut gizmos: Gizmos, blueprints: Res<Blueprints>) {
445 for (tile, kind) in &blueprints.0 {
446 let color = match kind {
447 BuildKind::Structure(StructureKind::Door) => DOOR_BLUEPRINT_COLOR,
448 BuildKind::Roof => ROOF_BLUEPRINT_COLOR,
449 BuildKind::Pave => PAVE_BLUEPRINT_COLOR,
450 _ => WALL_BLUEPRINT_COLOR,
451 };
452 gizmos.rect_2d(
453 WorldMap::tile_center(*tile),
454 Vec2::splat(TILE_SIZE * 0.85),
455 color,
456 );
457 }
458 }
459
460 #[cfg(test)]
461 mod tests {
462 use super::*;
463 use crate::world::Terrain;
464
465 fn ctx<'a>(map: &'a WorldMap, structures: &'a [(UVec2, StructureKind)]) -> ShelterContext<'a> {
466 ShelterContext {
467 map,
468 structures,
469 items: &[],
470 }
471 }
472
473 fn count(plan: &HashMap<UVec2, BuildKind>, kind: BuildKind) -> usize {
474 plan.values().filter(|k| **k == kind).count()
475 }
476
477 const WALL: BuildKind = BuildKind::Structure(StructureKind::Wall);
478 const DOOR: BuildKind = BuildKind::Structure(StructureKind::Door);
479
480 #[test]
481 fn the_build_tool_cycles_through_structures_and_off() {
482 let mut tool = BuildTool::default();
483 for kind in BUILD_TOOL_CYCLE {
484 tool.cycle();
485 assert_eq!(tool.kind, Some(kind));
486 }
487 tool.cycle();
488 assert_eq!(tool.kind, None);
489 }
490
491 #[test]
492 fn dragging_walls_plans_a_room_outline_on_free_ground() {
493 let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass);
494 map.set_plant(UVec2::new(2, 2), Some(crate::world::Plant::Tree));
495 let mut zones = Zones::default();
496 zones.add_tiles(&map, [UVec2::new(5, 2)]);
497 let mut blueprints = Blueprints::default();
498 let occupied = HashSet::from([UVec2::new(5, 5)]);
499 let tiles = placements(StructureKind::Wall, UVec2::new(5, 5), UVec2::new(2, 2));
500 assert_eq!(tiles.len(), 12);
501 place_blueprints(
502 &map,
503 &mut zones,
504 &mut blueprints,
505 &occupied,
506 StructureKind::Wall,
507 &tiles,
508 );
509 assert_eq!(blueprints.0.len(), 10);
510 assert!(!zones.is_stockpile(UVec2::new(5, 2)));
511 assert_eq!(
512 placements(StructureKind::Campfire, UVec2::ZERO, UVec2::ONE),
513 vec![UVec2::ONE]
514 );
515 }
516
517 #[test]
518 fn shelter_is_a_wall_ring_with_one_door_around_the_beds() {
519 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
520 let beds = [UVec2::new(10, 10), UVec2::new(11, 10)];
521 let plan = ctx(&map, &[]).plan(&beds, UVec2::new(10, 2)).unwrap();
522 assert_eq!(plan.len(), 6 * 5 - 4 * 3);
523 assert_eq!(count(&plan, DOOR), 1);
524 let door = plan.iter().find(|(_, k)| **k == DOOR).unwrap().0;
525 assert_eq!(door.y, 8);
526 }
527
528 // Mountains are free walls: a shelter dug against a cliff only needs walls on the open sides.
529 #[test]
530 fn rock_counts_as_wall() {
531 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
532 for y in 0..20 {
533 map.set(UVec2::new(12, y), Terrain::Stone);
534 }
535 let plan = ctx(&map, &[])
536 .plan(&[UVec2::new(10, 10)], UVec2::new(10, 2))
537 .unwrap();
538 assert!(plan.keys().all(|t| t.x != 12));
539 assert_eq!(count(&plan, WALL) + count(&plan, DOOR), 16 - 5);
540 }
541
542 #[test]
543 fn shelters_route_around_unbuildable_tiles() {
544 let mut map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
545 map.set(UVec2::new(10, 8), Terrain::ShallowWater);
546 let plan = ctx(&map, &[])
547 .plan(&[UVec2::new(10, 10)], UVec2::new(10, 2))
548 .unwrap();
549 assert!(!plan.contains_key(&UVec2::new(10, 8)));
550 assert_eq!(
551 plan.values()
552 .filter(|k| **k == BuildKind::Structure(StructureKind::Door))
553 .count(),
554 1
555 );
556 }
557
558 // Beds often end up right next to the stockpile; walls may take over empty stockpile tiles
559 // (which then stop being storage) instead of the shelter never getting planned at all.
560 #[test]
561 fn walls_may_replace_empty_stockpile_tiles_but_not_stored_items() {
562 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
563 let mut zones = Zones::default();
564 zones.add_stockpile(&map, IVec2::new(12, 5), IVec2::new(15, 15));
565 let items = [UVec2::new(13, 10)];
566 let ctx = ShelterContext {
567 map: &map,
568 structures: &[],
569 items: &items,
570 };
571 let plan = ctx.plan(&[UVec2::new(11, 10)], UVec2::new(5, 10)).unwrap();
572 assert!(plan.keys().any(|t| zones.is_stockpile(*t)));
573 assert!(!plan.contains_key(&UVec2::new(13, 10)));
574 }
575
576 #[test]
577 fn enclosed_rooms_get_roof_blueprints() {
578 let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass);
579 for i in 3..=6 {
580 for t in [
581 UVec2::new(i, 3),
582 UVec2::new(i, 6),
583 UVec2::new(3, i),
584 UVec2::new(6, i),
585 ] {
586 map.set_wall(t, true);
587 }
588 }
589 map.set_wall(UVec2::new(4, 3), false);
590 let structures = [(UVec2::new(4, 3), StructureKind::Door)];
591 let plan = ctx(&map, &structures)
592 .plan(&[UVec2::new(4, 4)], UVec2::ZERO)
593 .unwrap();
594 assert_eq!(plan.len(), 4);
595 assert!(plan.values().all(|k| *k == BuildKind::Roof));
596 }
597
598 #[test]
599 fn open_areas_are_not_rooms() {
600 let map = WorldMap::filled(UVec2::splat(30), Terrain::Grass);
601 assert!(enclosed_room(&map, &HashSet::new(), UVec2::new(15, 15)).is_none());
602 }
603 }