Skip to main content

terrain_forge/effects/
connectivity.rs

1//! Connectivity effects
2
3use crate::grid::{line_points, Cell};
4use crate::semantic::{MarkerType, SemanticLayers};
5use crate::spatial::{shortest_path, PathfindingConstraints};
6use crate::{Grid, Rng, Tile};
7use std::collections::HashSet;
8use std::collections::VecDeque;
9
10/// Methods for connecting semantic markers
11#[derive(Debug, Clone, Copy)]
12/// Method for connecting markers.
13pub enum MarkerConnectMethod {
14    /// Connect with a straight line.
15    Line,
16    /// Connect with an L-shaped path.
17    Path,
18}
19
20/// Labels connected floor regions, returning (label grid, region count).
21pub fn label_regions(grid: &Grid<Tile>) -> (Vec<u32>, u32) {
22    let (w, h) = (grid.width(), grid.height());
23    let regions = grid.flood_regions();
24    let mut labels = vec![0u32; w * h];
25    for (i, region) in regions.iter().enumerate() {
26        let label = (i + 1) as u32;
27        for &(x, y) in region {
28            labels[y * w + x] = label;
29        }
30    }
31    (labels, regions.len() as u32)
32}
33
34/// Carve a path into the grid with an optional radius around each step.
35/// Carves a path of floor tiles with the given radius.
36pub fn carve_path(grid: &mut Grid<Tile>, path: &[(usize, usize)], radius: usize) {
37    if path.is_empty() {
38        return;
39    }
40
41    for &(x, y) in path {
42        carve_point(grid, x as i32, y as i32, radius);
43    }
44}
45
46/// Clear a rectangular area centered at `center` with size (w, h).
47/// Clears a rectangular area to floor.
48pub fn clear_rect(grid: &mut Grid<Tile>, center: (usize, usize), w: usize, h: usize) {
49    if w == 0 || h == 0 {
50        return;
51    }
52
53    let x = center.0 as i32 - (w as i32 / 2);
54    let y = center.1 as i32 - (h as i32 / 2);
55    grid.fill_rect(x, y, w, h, Tile::Floor);
56}
57
58/// Connect the first matching marker of each type.
59/// Connects marker positions using the specified method.
60pub fn connect_markers(
61    grid: &mut Grid<Tile>,
62    layers: &SemanticLayers,
63    from: &MarkerType,
64    to: &MarkerType,
65    method: MarkerConnectMethod,
66    radius: usize,
67) -> bool {
68    let from_pos = crate::semantic::marker_positions(layers, from);
69    let to_pos = crate::semantic::marker_positions(layers, to);
70
71    let start = match from_pos.first() {
72        Some(pos) => *pos,
73        None => return false,
74    };
75    let end = match to_pos.first() {
76        Some(pos) => *pos,
77        None => return false,
78    };
79
80    match method {
81        MarkerConnectMethod::Line => {
82            let path = line_points(start, end);
83            carve_path(grid, &path, radius);
84            true
85        }
86        MarkerConnectMethod::Path => {
87            let constraints = PathfindingConstraints::default();
88            if let Some(path) = shortest_path(grid, start, end, &constraints) {
89                carve_path(grid, &path, radius);
90                true
91            } else {
92                false
93            }
94        }
95    }
96}
97
98/// Connect regions using spanning tree with optional extra connections for loops
99/// Connects regions via spanning tree with optional extra loops.
100pub fn connect_regions_spanning(
101    grid: &mut Grid<Tile>,
102    extra_connection_chance: f64,
103    rng: &mut Rng,
104) -> Vec<(usize, usize)> {
105    let (w, h) = (grid.width(), grid.height());
106    let (labels, region_count) = label_regions(grid);
107
108    if region_count <= 1 {
109        return Vec::new();
110    }
111
112    // Build region adjacency list
113    let mut regions: Vec<Vec<(usize, usize)>> = vec![Vec::new(); region_count as usize + 1];
114    for y in 0..h {
115        for x in 0..w {
116            if grid[(x, y)].is_floor() {
117                regions[labels[y * w + x] as usize].push((x, y));
118            }
119        }
120    }
121
122    // Find all possible connectors (walls adjacent to 2+ regions)
123    let mut connectors = Vec::new();
124    for y in 1..h - 1 {
125        for x in 1..w - 1 {
126            if !grid[(x, y)].is_floor() {
127                let adjacent_regions: HashSet<u32> =
128                    [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]
129                        .iter()
130                        .filter_map(|&(nx, ny)| {
131                            if grid[(nx, ny)].is_floor() {
132                                Some(labels[ny * w + nx])
133                            } else {
134                                None
135                            }
136                        })
137                        .collect();
138
139                if adjacent_regions.len() >= 2 {
140                    connectors.push((x, y, adjacent_regions.into_iter().collect::<Vec<_>>()));
141                }
142            }
143        }
144    }
145
146    // Kruskal's algorithm for minimum spanning tree
147    let mut connected_regions = vec![false; region_count as usize + 1];
148    let mut connections_made = Vec::new();
149    let mut edges_used = 0;
150
151    // Shuffle connectors for randomness
152    rng.shuffle(&mut connectors);
153
154    for (x, y, adjacent) in &connectors {
155        // Check if this connector would connect unconnected regions
156        let unconnected: Vec<u32> = adjacent
157            .iter()
158            .filter(|&&r| !connected_regions[r as usize])
159            .copied()
160            .collect();
161
162        if !unconnected.is_empty() {
163            // Connect regions
164            grid.set(*x as i32, *y as i32, Tile::Floor);
165            connections_made.push((*x, *y));
166
167            for &region in &unconnected {
168                connected_regions[region as usize] = true;
169            }
170
171            edges_used += 1;
172            if edges_used >= region_count - 1 {
173                break;
174            }
175        } else if rng.chance(extra_connection_chance) {
176            // Add extra connection for loops
177            grid.set(*x as i32, *y as i32, Tile::Floor);
178            connections_made.push((*x, *y));
179        }
180    }
181
182    connections_made
183}
184
185/// Bridges small gaps between floor regions.
186pub fn bridge_gaps(grid: &mut Grid<Tile>, max_distance: usize) {
187    let regions = grid.flood_regions();
188    if regions.len() <= 1 {
189        return;
190    }
191
192    // Pre-filter to perimeter cells only (adjacent to at least one wall).
193    // This reduces O(n·m) comparisons dramatically since perimeter ≪ area.
194    let perimeters: Vec<Vec<(usize, usize)>> = regions
195        .iter()
196        .map(|region| {
197            region
198                .iter()
199                .copied()
200                .filter(|&(x, y)| {
201                    [(-1i32, 0), (1, 0), (0, -1), (0, 1)]
202                        .iter()
203                        .any(|&(dx, dy)| {
204                            !grid
205                                .get(x as i32 + dx, y as i32 + dy)
206                                .is_some_and(|c| c.is_passable())
207                        })
208                })
209                .collect()
210        })
211        .collect();
212
213    for r1 in 0..perimeters.len() {
214        for r2 in (r1 + 1)..perimeters.len() {
215            if let Some((x1, y1, x2, y2)) =
216                find_closest(&perimeters[r1], &perimeters[r2], max_distance)
217            {
218                carve_line(grid, x1, y1, x2, y2);
219            }
220        }
221    }
222}
223
224fn find_closest(
225    r1: &[(usize, usize)],
226    r2: &[(usize, usize)],
227    max_dist: usize,
228) -> Option<(usize, usize, usize, usize)> {
229    let mut best = None;
230    let mut best_dist = max_dist + 1;
231
232    for &(x1, y1) in r1 {
233        for &(x2, y2) in r2 {
234            let dist = ((x1 as i32 - x2 as i32).abs() + (y1 as i32 - y2 as i32).abs()) as usize;
235            if dist < best_dist {
236                best_dist = dist;
237                best = Some((x1, y1, x2, y2));
238            }
239        }
240    }
241    best
242}
243
244fn carve_line(grid: &mut Grid<Tile>, x1: usize, y1: usize, x2: usize, y2: usize) {
245    let path = line_points((x1, y1), (x2, y2));
246    carve_path(grid, &path, 0);
247}
248
249/// Removes dead-end corridors.
250pub fn remove_dead_ends(grid: &mut Grid<Tile>, iterations: usize) {
251    let (w, h) = (grid.width(), grid.height());
252
253    for _ in 0..iterations {
254        let mut changed = false;
255        for y in 1..h - 1 {
256            for x in 1..w - 1 {
257                if !grid[(x, y)].is_floor() {
258                    continue;
259                }
260                let neighbors = [
261                    grid[(x - 1, y)].is_floor(),
262                    grid[(x + 1, y)].is_floor(),
263                    grid[(x, y - 1)].is_floor(),
264                    grid[(x, y + 1)].is_floor(),
265                ];
266                if neighbors.iter().filter(|&&b| b).count() <= 1 {
267                    grid.set(x as i32, y as i32, Tile::Wall);
268                    changed = true;
269                }
270            }
271        }
272        if !changed {
273            break;
274        }
275    }
276}
277
278/// Finds chokepoint cells (removal would disconnect regions).
279pub fn find_chokepoints(grid: &Grid<Tile>) -> Vec<(usize, usize)> {
280    let (w, h) = (grid.width(), grid.height());
281    let mut chokepoints = Vec::new();
282
283    for y in 1..h - 1 {
284        for x in 1..w - 1 {
285            if !grid[(x, y)].is_floor() {
286                continue;
287            }
288
289            let neighbors: Vec<(usize, usize)> = [
290                (x.wrapping_sub(1), y),
291                (x + 1, y),
292                (x, y.wrapping_sub(1)),
293                (x, y + 1),
294            ]
295            .into_iter()
296            .filter(|&(nx, ny)| nx < w && ny < h && grid[(nx, ny)].is_floor())
297            .collect();
298
299            if neighbors.len() >= 2 {
300                let mut visited = vec![false; w * h];
301                visited[y * w + x] = true;
302
303                let start = neighbors[0];
304                let mut queue = VecDeque::new();
305                queue.push_back(start);
306                visited[start.1 * w + start.0] = true;
307
308                while let Some((cx, cy)) = queue.pop_front() {
309                    for (nx, ny) in [
310                        (cx.wrapping_sub(1), cy),
311                        (cx + 1, cy),
312                        (cx, cy.wrapping_sub(1)),
313                        (cx, cy + 1),
314                    ] {
315                        if nx < w && ny < h && !visited[ny * w + nx] && grid[(nx, ny)].is_floor() {
316                            visited[ny * w + nx] = true;
317                            queue.push_back((nx, ny));
318                        }
319                    }
320                }
321
322                if neighbors
323                    .iter()
324                    .skip(1)
325                    .any(|&(nx, ny)| !visited[ny * w + nx])
326                {
327                    chokepoints.push((x, y));
328                }
329            }
330        }
331    }
332    chokepoints
333}
334
335fn carve_point(grid: &mut Grid<Tile>, x: i32, y: i32, radius: usize) {
336    if radius == 0 {
337        grid.set(x, y, Tile::Floor);
338        return;
339    }
340
341    let r = radius as i32;
342    let r2 = r * r;
343    for dy in -r..=r {
344        for dx in -r..=r {
345            if dx * dx + dy * dy <= r2 {
346                grid.set(x + dx, y + dy, Tile::Floor);
347            }
348        }
349    }
350}