terrain_forge/algorithms/
room_accretion.rs1use crate::{Algorithm, Grid, Rng, Tile};
2
3#[derive(Debug, Clone)]
4pub struct RoomAccretionConfig {
5 pub templates: Vec<RoomTemplate>,
6 pub max_rooms: usize,
7 pub loop_chance: f64,
8}
9
10#[derive(Debug, Clone)]
11pub enum RoomTemplate {
12 Rectangle {
13 min: usize,
14 max: usize,
15 },
16 Blob {
17 size: usize,
18 smoothing: usize,
19 },
20 Circle {
21 min_radius: usize,
22 max_radius: usize,
23 },
24}
25
26impl Default for RoomAccretionConfig {
27 fn default() -> Self {
28 Self {
29 templates: vec![
30 RoomTemplate::Rectangle { min: 5, max: 12 },
31 RoomTemplate::Blob {
32 size: 8,
33 smoothing: 2,
34 },
35 RoomTemplate::Circle {
36 min_radius: 3,
37 max_radius: 6,
38 },
39 ],
40 max_rooms: 15,
41 loop_chance: 0.1,
42 }
43 }
44}
45
46pub struct RoomAccretion {
47 config: RoomAccretionConfig,
48}
49
50impl RoomAccretion {
51 pub fn new(config: RoomAccretionConfig) -> Self {
52 Self { config }
53 }
54}
55
56impl Default for RoomAccretion {
57 fn default() -> Self {
58 Self::new(RoomAccretionConfig::default())
59 }
60}
61
62impl Algorithm<Tile> for RoomAccretion {
63 fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
64 let mut rng = Rng::new(seed);
65 let (w, h) = (grid.width(), grid.height());
66
67 let center_x = w / 2;
69 let center_y = h / 2;
70 let template = rng.pick(&self.config.templates).unwrap().clone();
71 place_room(grid, &template, center_x, center_y, &mut rng);
72
73 for _ in 1..self.config.max_rooms {
75 let template = rng.pick(&self.config.templates).unwrap().clone();
76
77 let mut placed = false;
79 for _ in 0..50 {
80 let start_x = rng.range_usize(5, w - 5);
81 let start_y = rng.range_usize(5, h - 5);
82
83 if let Some((final_x, final_y)) =
84 slide_to_fit(grid, &template, start_x, start_y, &mut rng)
85 {
86 place_room(grid, &template, final_x, final_y, &mut rng);
87
88 connect_to_existing(grid, final_x, final_y, &template, &mut rng);
90 placed = true;
91 break;
92 }
93 }
94
95 if !placed {
96 break;
97 }
98 }
99
100 if self.config.loop_chance > 0.0 {
102 crate::effects::connect_regions_spanning(grid, self.config.loop_chance, &mut rng);
103 }
104 }
105
106 fn name(&self) -> &'static str {
107 "RoomAccretion"
108 }
109}
110
111fn place_room(grid: &mut Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize, rng: &mut Rng) {
112 match template {
113 RoomTemplate::Rectangle { min, max } => {
114 let size = rng.range_usize(*min, *max + 1);
115 let half = size / 2;
116 for y in cy.saturating_sub(half)..=(cy + half).min(grid.height() - 1) {
117 for x in cx.saturating_sub(half)..=(cx + half).min(grid.width() - 1) {
118 grid.set(x as i32, y as i32, Tile::Floor);
119 }
120 }
121 }
122 RoomTemplate::Circle {
123 min_radius,
124 max_radius,
125 } => {
126 let radius = rng.range_usize(*min_radius, *max_radius + 1);
127 let r2 = (radius * radius) as f64;
128 for dy in -(radius as i32)..=(radius as i32) {
129 for dx in -(radius as i32)..=(radius as i32) {
130 if (dx * dx + dy * dy) as f64 <= r2 {
131 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
132 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
133 grid.set(x as i32, y as i32, Tile::Floor);
134 }
135 }
136 }
137 }
138 RoomTemplate::Blob { size, smoothing } => {
139 let half = size / 2;
141 let mut temp_grid = Grid::new(size + 2, size + 2);
142
143 for y in 1..size + 1 {
145 for x in 1..size + 1 {
146 if rng.chance(0.45) {
147 temp_grid.set(x as i32, y as i32, Tile::Floor);
148 }
149 }
150 }
151
152 for _ in 0..*smoothing {
154 let mut new_grid = temp_grid.clone();
155 for y in 1..size + 1 {
156 for x in 1..size + 1 {
157 let neighbors = [
158 temp_grid[(x - 1, y)],
159 temp_grid[(x + 1, y)],
160 temp_grid[(x, y - 1)],
161 temp_grid[(x, y + 1)],
162 temp_grid[(x - 1, y - 1)],
163 temp_grid[(x + 1, y - 1)],
164 temp_grid[(x - 1, y + 1)],
165 temp_grid[(x + 1, y + 1)],
166 ];
167 let floor_count = neighbors.iter().filter(|t| t.is_floor()).count();
168 new_grid.set(
169 x as i32,
170 y as i32,
171 if floor_count >= 4 {
172 Tile::Floor
173 } else {
174 Tile::Wall
175 },
176 );
177 }
178 }
179 temp_grid = new_grid;
180 }
181
182 for y in 1..size + 1 {
184 for x in 1..size + 1 {
185 if temp_grid[(x, y)].is_floor() {
186 let gx = (cx as i32 + x as i32 - half as i32 - 1)
187 .max(0)
188 .min(grid.width() as i32 - 1);
189 let gy = (cy as i32 + y as i32 - half as i32 - 1)
190 .max(0)
191 .min(grid.height() as i32 - 1);
192 grid.set(gx, gy, Tile::Floor);
193 }
194 }
195 }
196 }
197 }
198}
199
200fn slide_to_fit(
201 grid: &Grid<Tile>,
202 template: &RoomTemplate,
203 start_x: usize,
204 start_y: usize,
205 rng: &mut Rng,
206) -> Option<(usize, usize)> {
207 let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)]; let direction = rng.pick(&directions).unwrap();
209
210 let mut x = start_x as i32;
211 let mut y = start_y as i32;
212
213 for _ in 0..50 {
215 if would_overlap(grid, template, x as usize, y as usize) {
216 x -= direction.0;
218 y -= direction.1;
219 if is_adjacent_to_floor(grid, template, x as usize, y as usize) {
220 return Some((x as usize, y as usize));
221 }
222 return None;
223 }
224
225 x += direction.0;
226 y += direction.1;
227
228 if x < 5 || y < 5 || x >= grid.width() as i32 - 5 || y >= grid.height() as i32 - 5 {
229 return None;
230 }
231 }
232
233 None
234}
235
236fn would_overlap(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
237 let bounds = get_template_bounds(template);
238 for dy in -bounds.1..=bounds.1 {
239 for dx in -bounds.0..=bounds.0 {
240 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
241 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
242 if grid[(x, y)].is_floor() {
243 return true;
244 }
245 }
246 }
247 false
248}
249
250fn is_adjacent_to_floor(grid: &Grid<Tile>, template: &RoomTemplate, cx: usize, cy: usize) -> bool {
251 let bounds = get_template_bounds(template);
252 for dy in -(bounds.1 + 1)..=(bounds.1 + 1) {
253 for dx in -(bounds.0 + 1)..=(bounds.0 + 1) {
254 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
255 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
256 if grid[(x, y)].is_floor() {
257 return true;
258 }
259 }
260 }
261 false
262}
263
264fn get_template_bounds(template: &RoomTemplate) -> (i32, i32) {
265 match template {
266 RoomTemplate::Rectangle { max, .. } => ((*max / 2) as i32, (*max / 2) as i32),
267 RoomTemplate::Circle { max_radius, .. } => (*max_radius as i32, *max_radius as i32),
268 RoomTemplate::Blob { size, .. } => ((size / 2) as i32, (size / 2) as i32),
269 }
270}
271
272fn connect_to_existing(
273 grid: &mut Grid<Tile>,
274 cx: usize,
275 cy: usize,
276 template: &RoomTemplate,
277 rng: &mut Rng,
278) {
279 let bounds = get_template_bounds(template);
280
281 let mut edge_points = Vec::new();
283 for dy in -bounds.1..=bounds.1 {
284 for dx in -bounds.0..=bounds.0 {
285 let x = (cx as i32 + dx).max(0).min(grid.width() as i32 - 1) as usize;
286 let y = (cy as i32 + dy).max(0).min(grid.height() as i32 - 1) as usize;
287 if grid[(x, y)].is_floor() {
288 let neighbors = [
290 (x.wrapping_sub(1), y),
291 (x + 1, y),
292 (x, y.wrapping_sub(1)),
293 (x, y + 1),
294 ];
295 for &(nx, ny) in &neighbors {
296 if nx < grid.width() && ny < grid.height() && !grid[(nx, ny)].is_floor() {
297 edge_points.push((x, y));
298 break;
299 }
300 }
301 }
302 }
303 }
304
305 if let Some(&(start_x, start_y)) = rng.pick(&edge_points) {
306 let directions = [(0, -1), (1, 0), (0, 1), (-1, 0)];
308 let direction = rng.pick(&directions).unwrap();
309
310 for i in 1..=3 {
311 let x = (start_x as i32 + direction.0 * i)
312 .max(0)
313 .min(grid.width() as i32 - 1);
314 let y = (start_y as i32 + direction.1 * i)
315 .max(0)
316 .min(grid.height() as i32 - 1);
317 grid.set(x, y, Tile::Floor);
318 }
319 }
320}