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