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