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