1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use all_is_cubes::euclid::Size3D;
use all_is_cubes::linking::InGenError;
use all_is_cubes::math::{
Cube, Face6, FaceMap, GridAab, GridCoordinate, GridSize, GridSizeCoord, GridVector,
VectorOps as _, Vol,
};
use all_is_cubes::space::Space;
use all_is_cubes::util::YieldProgress;
/// Defines the dimensions that dungeon room construction must live within.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DungeonGrid {
/// The basic room interior which is going to be duplicated to form the room grid.
/// This _does not_ include the given wall thickness.
pub room_box: GridAab,
/// Thickness of each wall of the room, located outside of `room_box` but still
/// belonging to that room and not its neighbors.
///
/// TODO: `FaceMap` is a _possible_ representation but not the _most robust_ and we
/// should instead make this a `GridAab` and add a `GridAab::minkowski_sum` method.
pub room_wall_thickness: FaceMap<u16>,
/// Thickness of the space lying between each pair of rooms, belonging to neither.
pub gap_between_walls: Size3D<u16, Cube>,
}
impl DungeonGrid {
/// Returns the distance from the end of one room interior to the beginning of
/// another, along each axis.
#[rustfmt::skip]
pub fn gap_between_rooms(&self) -> GridSize {
GridSize::from(self.gap_between_walls.map(GridSizeCoord::from).to_vector()
+ self.room_wall_thickness.negatives().map(GridSizeCoord::from)
+ self.room_wall_thickness.positives().map(GridSizeCoord::from))
}
/// Returns the distances from one room to the same point on the next room, along each axis.
pub fn room_spacing(&self) -> GridSize {
self.room_box.size() + self.gap_between_rooms()
}
/// Returns the translation which would be applied to move `self.room_box` to the
/// location of a specific room.
pub fn room_translation(&self, room_position: Cube) -> GridVector {
room_position
.lower_bounds()
.to_vector()
.component_mul(self.room_spacing().to_vector().to_i32())
}
pub fn room_box_including_walls(&self) -> GridAab {
self.room_box
.expand(self.room_wall_thickness.map(|_, c| c.into()))
}
pub fn room_box_at(&self, room_position: Cube) -> GridAab {
self.room_box
.translate(self.room_translation(room_position))
}
/// Returns the volume which lies between two rooms and meets their adjoining faces.
#[expect(dead_code)] // TODO: superseded in use by theme-specific sizes; review if should keep
pub fn shared_wall_at(&self, room_position: Cube, face: Face6) -> GridAab {
self.room_box_at(room_position)
.abut(
face,
GridCoordinate::from(self.room_wall_thickness[face])
+ GridCoordinate::from(self.room_wall_thickness[face.opposite()])
+ GridCoordinate::from(self.gap_between_walls[face.axis()]),
)
.unwrap()
}
/// Compute the minimum size of `Space` that will fit all the rooms with coordinates
/// in `rooms`.
///
/// This includes the `room_wall_thickness` of the outermost rooms, but does not
/// include a `gap_between_walls` outside of that, since that may not be wanted and
/// is easy to add using [`GridAab::expand()`].
///
/// TODO: This is off by at least 1 (in the too-big direction); write tests and fix.
pub fn minimum_space_for_rooms(&self, rooms: GridAab) -> GridAab {
let spacing = self.room_spacing().to_i32().to_vector();
let basic_size = GridAab::from_lower_upper(
rooms
.lower_bounds()
.to_vector()
.component_mul(spacing)
.to_point(),
rooms
.upper_bounds()
.to_vector()
.component_mul(spacing)
.to_point()
// Correct "fencepost error": spacing * number of rooms has one extra
// "post" (gap_between_walls).
- self.gap_between_walls.map(GridCoordinate::from),
);
// basic_size has the correct size, but not the correct position relative to room_box
// and its walls
basic_size.translate(self.room_box_including_walls().lower_bounds().to_vector())
}
}
pub trait Theme<R> {
fn passes(&self) -> usize;
// TODO: Replace `&mut Space` with transactions so we can use this post-startup?
fn place_room(
&self,
space: &mut Space,
pass_index: usize,
map: Vol<&[R]>,
position_in_room_grid: Cube,
value: &R,
) -> Result<(), InGenError>;
}
pub async fn build_dungeon<Room, ThemeT: Theme<Room>>(
space: &mut Space,
theme: &ThemeT,
map: Vol<&[Room]>,
progress: YieldProgress,
) -> Result<(), InGenError> {
let passes = theme.passes();
for (pass, mut progress) in (0..passes).zip(progress.split_evenly(passes)) {
progress.set_label(format_args!("pass {pass}/{passes}", pass = pass + 1));
progress.progress(0.0).await;
for (room_position, mut progress) in map
.bounds()
.interior_iter()
.zip(progress.split_evenly(map.volume()))
{
progress.set_label(format_args!(
"pass {pass} @ {room_position:?}",
pass = pass + 1
));
progress.progress(0.0).await;
theme.place_room(
space,
pass,
map,
room_position,
map.get(room_position).unwrap(),
)?;
progress.progress(1.0).await;
}
}
Ok(())
}