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