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