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