terrain_forge/algorithms/
glass_seam.rs

1use crate::effects::carve_path;
2use crate::{Algorithm, Grid, Rng, Tile};
3use std::collections::{HashSet, VecDeque};
4
5#[derive(Debug, Clone)]
6pub struct GlassSeamConfig {
7    pub coverage_threshold: f64,
8    pub required_points: Vec<(usize, usize)>,
9    pub carve_radius: usize,
10    pub use_mst_terminals: bool,
11}
12
13impl Default for GlassSeamConfig {
14    fn default() -> Self {
15        Self {
16            coverage_threshold: 0.75,
17            required_points: Vec::new(),
18            carve_radius: 0,
19            use_mst_terminals: true,
20        }
21    }
22}
23
24#[derive(Default)]
25pub struct GlassSeam {
26    pub config: GlassSeamConfig,
27}
28
29impl GlassSeam {
30    pub fn new(config: GlassSeamConfig) -> Self {
31        Self { config }
32    }
33}
34
35impl Algorithm<Tile> for GlassSeam {
36    fn generate(&self, grid: &mut Grid<Tile>, seed: u64) {
37        let mut rng = Rng::new(seed);
38
39        // Glass Seam Bridging should only connect existing regions, not create new patterns
40        // The grid should already have floor tiles from a previous algorithm
41
42        // Find spawn point (first required point or first floor tile)
43        let spawn = self
44            .config
45            .required_points
46            .iter()
47            .copied()
48            .find(|&(x, y)| {
49                grid.get(x as i32, y as i32)
50                    .is_some_and(|tile| tile.is_floor())
51            })
52            .or_else(|| find_spawn_point(grid))
53            .unwrap_or((5, 5));
54
55        // Ensure connectivity between existing regions
56        ensure_connectivity(grid, spawn, &self.config, &mut rng);
57    }
58
59    fn name(&self) -> &'static str {
60        "GlassSeam"
61    }
62}
63
64fn find_spawn_point(grid: &Grid<Tile>) -> Option<(usize, usize)> {
65    for y in 0..grid.height() {
66        for x in 0..grid.width() {
67            if grid[(x, y)].is_floor() {
68                return Some((x, y));
69            }
70        }
71    }
72    None
73}
74
75fn ensure_connectivity(
76    grid: &mut Grid<Tile>,
77    spawn: (usize, usize),
78    config: &GlassSeamConfig,
79    _rng: &mut Rng,
80) {
81    let RegionData {
82        regions,
83        labels,
84        width,
85    } = identify_regions(grid);
86    if regions.len() <= 1 {
87        return;
88    }
89
90    let spawn_region = match region_for_point(&labels, width, spawn) {
91        Some(region) => region,
92        None => return,
93    };
94    let total_floor: usize = regions.iter().map(|r| r.len()).sum();
95    let mut connected: HashSet<usize> = HashSet::new();
96    connected.insert(spawn_region);
97    let mut coverage = coverage_for_regions(&regions, &connected, total_floor);
98
99    if coverage >= config.coverage_threshold {
100        return;
101    }
102
103    if config.use_mst_terminals {
104        let required_regions =
105            required_regions(&labels, width, &config.required_points, spawn_region);
106        if required_regions.len() > 1 {
107            let edges = mst_edges(&required_regions, &regions);
108            for (a, b) in edges {
109                connect_regions(grid, &regions[a], &regions[b], config.carve_radius);
110                connected.insert(a);
111                connected.insert(b);
112            }
113            coverage = coverage_for_regions(&regions, &connected, total_floor);
114        }
115    }
116
117    while coverage < config.coverage_threshold && connected.len() < regions.len() {
118        let mut best = None;
119        let mut best_cost = usize::MAX;
120
121        for (i, region) in regions.iter().enumerate() {
122            if connected.contains(&i) {
123                continue;
124            }
125            for &ci in &connected {
126                let cost = connection_cost(&regions[ci], region);
127                if cost < best_cost {
128                    best_cost = cost;
129                    best = Some((i, ci));
130                }
131            }
132        }
133
134        if let Some((target, source)) = best {
135            connect_regions(
136                grid,
137                &regions[source],
138                &regions[target],
139                config.carve_radius,
140            );
141            connected.insert(target);
142            coverage = coverage_for_regions(&regions, &connected, total_floor);
143        } else {
144            break;
145        }
146    }
147}
148
149struct RegionData {
150    regions: Vec<Vec<(usize, usize)>>,
151    labels: Vec<u32>,
152    width: usize,
153}
154
155fn identify_regions(grid: &Grid<Tile>) -> RegionData {
156    let (w, h) = (grid.width(), grid.height());
157    let mut visited = vec![vec![false; h]; w];
158    let mut labels = vec![0u32; w * h];
159    let mut label = 0u32;
160    let mut regions = Vec::new();
161
162    for x in 0..w {
163        for y in 0..h {
164            if !visited[x][y] && grid[(x, y)].is_floor() {
165                label = label.wrapping_add(1).max(1);
166                let region = flood_fill(grid, x, y, &mut visited, &mut labels, w, label);
167                if !region.is_empty() {
168                    regions.push(region);
169                }
170            }
171        }
172    }
173
174    RegionData {
175        regions,
176        labels,
177        width: w,
178    }
179}
180
181fn flood_fill(
182    grid: &Grid<Tile>,
183    sx: usize,
184    sy: usize,
185    visited: &mut [Vec<bool>],
186    labels: &mut [u32],
187    width: usize,
188    label: u32,
189) -> Vec<(usize, usize)> {
190    let (w, h) = (grid.width(), grid.height());
191    let mut region = Vec::new();
192    let mut queue = VecDeque::new();
193    queue.push_back((sx, sy));
194
195    while let Some((x, y)) = queue.pop_front() {
196        if x >= w || y >= h || visited[x][y] || !grid[(x, y)].is_floor() {
197            continue;
198        }
199        visited[x][y] = true;
200        region.push((x, y));
201        labels[y * width + x] = label;
202
203        if x > 0 {
204            queue.push_back((x - 1, y));
205        }
206        if x + 1 < w {
207            queue.push_back((x + 1, y));
208        }
209        if y > 0 {
210            queue.push_back((x, y - 1));
211        }
212        if y + 1 < h {
213            queue.push_back((x, y + 1));
214        }
215    }
216    region
217}
218
219fn region_for_point(labels: &[u32], width: usize, point: (usize, usize)) -> Option<usize> {
220    if width == 0 {
221        return None;
222    }
223    let height = labels.len() / width;
224    if point.0 >= width || point.1 >= height {
225        return None;
226    }
227    let idx = point.1 * width + point.0;
228    let label = *labels.get(idx)?;
229    if label == 0 {
230        None
231    } else {
232        Some((label - 1) as usize)
233    }
234}
235
236fn required_regions(
237    labels: &[u32],
238    width: usize,
239    points: &[(usize, usize)],
240    spawn_region: usize,
241) -> Vec<usize> {
242    let mut set = HashSet::new();
243    set.insert(spawn_region);
244    for &point in points {
245        if let Some(region) = region_for_point(labels, width, point) {
246            set.insert(region);
247        }
248    }
249    set.into_iter().collect()
250}
251
252fn coverage_for_regions(
253    regions: &[Vec<(usize, usize)>],
254    connected: &HashSet<usize>,
255    total: usize,
256) -> f64 {
257    if total == 0 {
258        return 0.0;
259    }
260    let connected_cells: usize = connected.iter().map(|&idx| regions[idx].len()).sum();
261    connected_cells as f64 / total as f64
262}
263
264fn mst_edges(required: &[usize], regions: &[Vec<(usize, usize)>]) -> Vec<(usize, usize)> {
265    if required.len() < 2 {
266        return Vec::new();
267    }
268
269    let mut in_tree = HashSet::new();
270    in_tree.insert(required[0]);
271    let mut edges = Vec::new();
272
273    while in_tree.len() < required.len() {
274        let mut best = None;
275        let mut best_cost = usize::MAX;
276
277        for &a in &in_tree {
278            for &b in required {
279                if in_tree.contains(&b) {
280                    continue;
281                }
282                let cost = connection_cost(&regions[a], &regions[b]);
283                if cost < best_cost {
284                    best_cost = cost;
285                    best = Some((a, b));
286                }
287            }
288        }
289
290        if let Some((a, b)) = best {
291            edges.push((a, b));
292            in_tree.insert(b);
293        } else {
294            break;
295        }
296    }
297
298    edges
299}
300
301fn connection_cost(a: &[(usize, usize)], b: &[(usize, usize)]) -> usize {
302    let ca = centroid(a);
303    let cb = centroid(b);
304    ((ca.0 as i32 - cb.0 as i32).abs() + (ca.1 as i32 - cb.1 as i32).abs()) as usize
305}
306
307fn centroid(region: &[(usize, usize)]) -> (usize, usize) {
308    if region.is_empty() {
309        return (0, 0);
310    }
311    let sx: usize = region.iter().map(|p| p.0).sum();
312    let sy: usize = region.iter().map(|p| p.1).sum();
313    (sx / region.len(), sy / region.len())
314}
315
316fn connect_regions(
317    grid: &mut Grid<Tile>,
318    source: &[(usize, usize)],
319    target: &[(usize, usize)],
320    radius: usize,
321) {
322    let from = centroid(source);
323    let to = centroid(target);
324
325    let path = line_points(from, to);
326    carve_path(grid, &path, radius);
327}
328
329fn line_points(start: (usize, usize), end: (usize, usize)) -> Vec<(usize, usize)> {
330    let (mut x, mut y) = (start.0 as i32, start.1 as i32);
331    let (tx, ty) = (end.0 as i32, end.1 as i32);
332    let mut points = Vec::new();
333
334    while x != tx || y != ty {
335        if x >= 0 && y >= 0 {
336            points.push((x as usize, y as usize));
337        }
338        if (x - tx).abs() > (y - ty).abs() {
339            x += if tx > x { 1 } else { -1 };
340        } else {
341            y += if ty > y { 1 } else { -1 };
342        }
343    }
344    if tx >= 0 && ty >= 0 {
345        points.push((tx as usize, ty as usize));
346    }
347    points
348}