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