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