src/building.rs 23.3 K · 706 lines · raw · history

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