Skip to main content

proof_engine/worldgen/
tectonics.rs

1//! Tectonic plate simulation — rigid body plates on a 2D grid.
2//!
3//! Plates are generated via Voronoi partitioning, then drift according to
4//! velocity vectors. Collisions at plate boundaries generate mountains
5//! (convergent), rifts (divergent), or transform faults (shear).
6
7use super::{Grid2D, Rng, GridPos};
8use std::collections::HashMap;
9
10/// A tectonic plate.
11#[derive(Debug, Clone)]
12pub struct Plate {
13    pub id: u32,
14    /// Velocity vector (cells per step).
15    pub velocity: (f32, f32),
16    /// Whether this is an oceanic (true) or continental (false) plate.
17    pub oceanic: bool,
18    /// Average elevation (before collision effects).
19    pub base_elevation: f32,
20    /// Area in grid cells.
21    pub area: usize,
22    /// Density (oceanic is denser → subducts under continental).
23    pub density: f32,
24}
25
26/// The plate assignment map (which plate each cell belongs to).
27#[derive(Debug, Clone)]
28pub struct PlateMap {
29    pub width: usize,
30    pub height: usize,
31    /// Plate ID per cell.
32    pub assignment: Vec<u32>,
33    /// All plates.
34    pub plates: Vec<Plate>,
35}
36
37impl PlateMap {
38    pub fn plate_at(&self, x: usize, y: usize) -> u32 {
39        self.assignment[y * self.width + x]
40    }
41
42    /// Get boundary cells (cells adjacent to a different plate).
43    pub fn boundary_cells(&self) -> Vec<(usize, usize, u32, u32)> {
44        let mut boundaries = Vec::new();
45        for y in 0..self.height {
46            for x in 0..self.width {
47                let pid = self.plate_at(x, y);
48                for &(nx, ny) in &[(x.wrapping_sub(1), y), (x + 1, y), (x, y.wrapping_sub(1)), (x, y + 1)] {
49                    if nx < self.width && ny < self.height {
50                        let npid = self.plate_at(nx, ny);
51                        if npid != pid {
52                            boundaries.push((x, y, pid, npid));
53                            break;
54                        }
55                    }
56                }
57            }
58        }
59        boundaries
60    }
61}
62
63/// Boundary interaction type.
64#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum BoundaryType {
66    /// Plates colliding → mountains.
67    Convergent,
68    /// Plates separating → rifts/volcanos.
69    Divergent,
70    /// Plates sliding past → transform faults.
71    Transform,
72}
73
74/// Generate tectonic plates and base heightmap.
75pub fn generate(grid_size: usize, num_plates: usize, rng: &mut Rng) -> (Grid2D, PlateMap) {
76    let w = grid_size;
77    let h = grid_size;
78
79    // 1. Generate plate seeds (Voronoi centers)
80    let mut centers: Vec<(usize, usize)> = Vec::with_capacity(num_plates);
81    for _ in 0..num_plates {
82        centers.push((rng.range_usize(0, w), rng.range_usize(0, h)));
83    }
84
85    // 2. Voronoi assignment (each cell → nearest plate center)
86    let mut assignment = vec![0u32; w * h];
87    for y in 0..h {
88        for x in 0..w {
89            let mut best_dist = i32::MAX;
90            let mut best_plate = 0u32;
91            for (i, &(cx, cy)) in centers.iter().enumerate() {
92                // Wrap-around distance for toroidal topology
93                let dx = wrap_dist(x as i32, cx as i32, w as i32);
94                let dy = wrap_dist(y as i32, cy as i32, h as i32);
95                let d = dx * dx + dy * dy;
96                if d < best_dist {
97                    best_dist = d;
98                    best_plate = i as u32;
99                }
100            }
101            assignment[y * w + x] = best_plate;
102        }
103    }
104
105    // 3. Create plate structs
106    let mut plates: Vec<Plate> = (0..num_plates)
107        .map(|i| {
108            let oceanic = rng.coin(0.6);
109            Plate {
110                id: i as u32,
111                velocity: (rng.range_f32(-1.0, 1.0), rng.range_f32(-1.0, 1.0)),
112                oceanic,
113                base_elevation: if oceanic { rng.range_f32(-0.3, 0.1) } else { rng.range_f32(0.2, 0.6) },
114                area: 0,
115                density: if oceanic { 3.0 } else { 2.7 },
116            }
117        })
118        .collect();
119
120    // Count areas
121    for &pid in &assignment {
122        plates[pid as usize].area += 1;
123    }
124
125    let plate_map = PlateMap { width: w, height: h, assignment, plates: plates.clone() };
126
127    // 4. Generate heightmap from plate collision
128    let mut heightmap = Grid2D::new(w, h);
129
130    // Base elevation from plate type
131    for y in 0..h {
132        for x in 0..w {
133            let pid = plate_map.plate_at(x, y) as usize;
134            heightmap.set(x, y, plates[pid].base_elevation);
135        }
136    }
137
138    // Boundary effects
139    let boundaries = plate_map.boundary_cells();
140    for &(x, y, pid_a, pid_b) in &boundaries {
141        let pa = &plates[pid_a as usize];
142        let pb = &plates[pid_b as usize];
143        let bt = classify_boundary(pa, pb);
144
145        let elevation_boost = match bt {
146            BoundaryType::Convergent => {
147                // Mountain building
148                if pa.oceanic && !pb.oceanic {
149                    0.4 // Subduction → volcanic arc
150                } else if !pa.oceanic && !pb.oceanic {
151                    0.6 // Continental collision → high mountains
152                } else {
153                    0.2 // Ocean-ocean → island arc
154                }
155            }
156            BoundaryType::Divergent => {
157                -0.2 // Rift valley / mid-ocean ridge
158            }
159            BoundaryType::Transform => {
160                0.05 // Slight uplift
161            }
162        };
163
164        // Apply with falloff
165        let radius = 5;
166        for dy in -radius..=radius {
167            for dx in -radius..=radius {
168                let nx = (x as i32 + dx).rem_euclid(w as i32) as usize;
169                let ny = (y as i32 + dy).rem_euclid(h as i32) as usize;
170                let dist = ((dx * dx + dy * dy) as f32).sqrt();
171                let falloff = (1.0 - dist / radius as f32).max(0.0);
172                heightmap.add(nx, ny, elevation_boost * falloff * falloff);
173            }
174        }
175    }
176
177    // Add fractal noise for detail
178    add_fractal_noise(&mut heightmap, rng, 6, 0.3);
179
180    heightmap.normalize();
181
182    (heightmap, plate_map)
183}
184
185/// Classify boundary type from plate velocities.
186fn classify_boundary(a: &Plate, b: &Plate) -> BoundaryType {
187    // Relative velocity
188    let rel_vx = a.velocity.0 - b.velocity.0;
189    let rel_vy = a.velocity.1 - b.velocity.1;
190    let speed = (rel_vx * rel_vx + rel_vy * rel_vy).sqrt();
191
192    if speed < 0.3 {
193        BoundaryType::Transform
194    } else {
195        // Dot product with boundary normal (approximated)
196        let convergence = -(rel_vx * rel_vx + rel_vy * rel_vy).sqrt();
197        if convergence < -0.2 {
198            BoundaryType::Convergent
199        } else {
200            BoundaryType::Divergent
201        }
202    }
203}
204
205/// Wrap-around distance for toroidal grid.
206fn wrap_dist(a: i32, b: i32, size: i32) -> i32 {
207    let d = (a - b).abs();
208    d.min(size - d)
209}
210
211/// Add fractal Brownian motion noise to a grid.
212fn add_fractal_noise(grid: &mut Grid2D, rng: &mut Rng, octaves: usize, amplitude: f32) {
213    let w = grid.width;
214    let h = grid.height;
215    let base_seed = rng.next_u64();
216
217    for octave in 0..octaves {
218        let freq = (1 << octave) as f32;
219        let amp = amplitude / (1 << octave) as f32;
220        let seed = base_seed.wrapping_add(octave as u64 * 1000);
221
222        for y in 0..h {
223            for x in 0..w {
224                let nx = x as f32 * freq / w as f32;
225                let ny = y as f32 * freq / h as f32;
226                let noise = value_noise_2d(nx, ny, seed);
227                grid.add(x, y, noise * amp);
228            }
229        }
230    }
231}
232
233/// Simple value noise.
234fn value_noise_2d(x: f32, y: f32, seed: u64) -> f32 {
235    let xi = x.floor() as i64;
236    let yi = y.floor() as i64;
237    let xf = x - x.floor();
238    let yf = y - y.floor();
239    let tx = xf * xf * (3.0 - 2.0 * xf);
240    let ty = yf * yf * (3.0 - 2.0 * yf);
241
242    let hash = |ix: i64, iy: i64| -> f32 {
243        let n = (ix.wrapping_mul(374761393) + iy.wrapping_mul(668265263) + seed as i64) as u64;
244        let n = n.wrapping_mul(0x5851F42D4C957F2D);
245        let n = n ^ (n >> 32);
246        (n & 0x00FF_FFFF) as f32 / 0x0080_0000 as f32 - 1.0
247    };
248
249    let v00 = hash(xi, yi);
250    let v10 = hash(xi + 1, yi);
251    let v01 = hash(xi, yi + 1);
252    let v11 = hash(xi + 1, yi + 1);
253    let a = v00 + tx * (v10 - v00);
254    let b = v01 + tx * (v11 - v01);
255    a + ty * (b - a)
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_generate_plates() {
264        let mut rng = Rng::new(42);
265        let (hm, pm) = generate(64, 6, &mut rng);
266        assert_eq!(hm.width, 64);
267        assert_eq!(pm.plates.len(), 6);
268        // All cells assigned
269        assert!(pm.assignment.iter().all(|&p| (p as usize) < 6));
270    }
271
272    #[test]
273    fn test_boundaries_exist() {
274        let mut rng = Rng::new(42);
275        let (_, pm) = generate(64, 6, &mut rng);
276        let boundaries = pm.boundary_cells();
277        assert!(!boundaries.is_empty(), "should have plate boundaries");
278    }
279
280    #[test]
281    fn test_heightmap_normalized() {
282        let mut rng = Rng::new(42);
283        let (hm, _) = generate(64, 6, &mut rng);
284        let min = hm.min_value();
285        let max = hm.max_value();
286        assert!(min >= -0.01, "min should be ~0: {min}");
287        assert!(max <= 1.01, "max should be ~1: {max}");
288    }
289}