terrain_forge/algorithms/
room_accretion.rs

1use 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        // Start with first room in center
68        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        // Add rooms by sliding until they fit adjacent to existing structure
74        for _ in 1..self.config.max_rooms {
75            let template = rng.pick(&self.config.templates).unwrap().clone();
76
77            // Try multiple positions
78            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 structure
89                    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        // Add loops
101        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            // Create blob using cellular automata
140            let half = size / 2;
141            let mut temp_grid = Grid::new(size + 2, size + 2);
142
143            // Random fill
144            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            // Smooth
153            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            // Copy to main grid
183            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)]; // N, E, S, W
208    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    // Slide until we hit existing floor or boundary
214    for _ in 0..50 {
215        if would_overlap(grid, template, x as usize, y as usize) {
216            // Back up one step and check if adjacent
217            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    // Find edge of room
282    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                // Check if on edge
289                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        // Carve a short corridor
307        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}