Skip to main content

terrain_forge/effects/
connectivity.rs

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