terrain_forge/algorithms/
glass_seam.rs1use 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 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(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(®ions, &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, ®ions);
108 for (a, b) in edges {
109 connect_regions(grid, ®ions[a], ®ions[b], config.carve_radius);
110 connected.insert(a);
111 connected.insert(b);
112 }
113 coverage = coverage_for_regions(®ions, &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(®ions[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 ®ions[source],
138 ®ions[target],
139 config.carve_radius,
140 );
141 connected.insert(target);
142 coverage = coverage_for_regions(®ions, &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(®ions[a], ®ions[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}