Skip to main content

proof_engine/worldgen/
rivers.rs

1//! River generation — water flow simulation from rainfall to ocean.
2//!
3//! Traces downhill flow from every cell, accumulates flow volume,
4//! and marks cells exceeding a threshold as river segments.
5
6use super::{Grid2D, Rng};
7
8/// A river segment.
9#[derive(Debug, Clone)]
10pub struct RiverSegment {
11    pub x: usize,
12    pub y: usize,
13    /// Accumulated flow volume at this cell.
14    pub flow: f32,
15    /// Width of the river at this point (derived from flow).
16    pub width: f32,
17    /// Downstream direction (dx, dy).
18    pub downstream: (i32, i32),
19}
20
21/// Complete river network.
22#[derive(Debug, Clone)]
23pub struct RiverNetwork {
24    pub segments: Vec<RiverSegment>,
25    /// Flow accumulation grid.
26    pub flow_grid: Grid2D,
27    /// River threshold (cells with flow > this are rivers).
28    pub threshold: f32,
29}
30
31impl RiverNetwork {
32    /// Check if a cell is a river.
33    pub fn is_river(&self, x: usize, y: usize) -> bool {
34        self.flow_grid.get(x, y) > self.threshold
35    }
36
37    /// Get river width at a cell.
38    pub fn width_at(&self, x: usize, y: usize) -> f32 {
39        let flow = self.flow_grid.get(x, y);
40        if flow > self.threshold {
41            (flow / self.threshold).sqrt().min(5.0)
42        } else {
43            0.0
44        }
45    }
46
47    /// Find river mouths (river cells adjacent to ocean).
48    pub fn mouths(&self, heightmap: &Grid2D, sea_level: f32) -> Vec<(usize, usize)> {
49        let mut mouths = Vec::new();
50        for seg in &self.segments {
51            let nx = (seg.x as i32 + seg.downstream.0).clamp(0, heightmap.width as i32 - 1) as usize;
52            let ny = (seg.y as i32 + seg.downstream.1).clamp(0, heightmap.height as i32 - 1) as usize;
53            if heightmap.get(nx, ny) < sea_level {
54                mouths.push((seg.x, seg.y));
55            }
56        }
57        mouths
58    }
59
60    /// Find the longest river path.
61    pub fn longest_river(&self) -> Vec<(usize, usize)> {
62        if self.segments.is_empty() { return Vec::new(); }
63        // Find segment with highest flow (likely near mouth)
64        let start = self.segments.iter().max_by(|a, b| a.flow.partial_cmp(&b.flow).unwrap()).unwrap();
65        // Trace upstream (not implemented in MVP — return single point)
66        vec![(start.x, start.y)]
67    }
68}
69
70/// Generate river network from heightmap and precipitation.
71pub fn generate(heightmap: &Grid2D, precipitation: &Grid2D, sea_level: f32) -> RiverNetwork {
72    let w = heightmap.width;
73    let h = heightmap.height;
74
75    // 1. Compute flow direction (steepest descent)
76    let mut flow_dir = vec![(0i32, 0i32); w * h];
77    for y in 0..h {
78        for x in 0..w {
79            let center = heightmap.get(x, y);
80            let mut best_dir = (0i32, 0i32);
81            let mut best_drop = 0.0_f32;
82
83            for &(dx, dy) in &[(-1i32, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (1, -1), (-1, 1), (1, 1)] {
84                let nx = x as i32 + dx;
85                let ny = y as i32 + dy;
86                if nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 { continue; }
87                let nh = heightmap.get(nx as usize, ny as usize);
88                let drop = center - nh;
89                let dist = if dx.abs() + dy.abs() == 2 { 1.414 } else { 1.0 };
90                let slope = drop / dist;
91                if slope > best_drop {
92                    best_drop = slope;
93                    best_dir = (dx, dy);
94                }
95            }
96            flow_dir[y * w + x] = best_dir;
97        }
98    }
99
100    // 2. Flow accumulation (each cell contributes its precipitation downstream)
101    let mut flow_grid = Grid2D::new(w, h);
102
103    // Initialize with precipitation
104    for y in 0..h {
105        for x in 0..w {
106            if heightmap.get(x, y) > sea_level {
107                flow_grid.set(x, y, precipitation.get(x, y));
108            }
109        }
110    }
111
112    // Sort cells by elevation (high to low) for single-pass accumulation
113    let mut sorted: Vec<(usize, usize, f32)> = Vec::with_capacity(w * h);
114    for y in 0..h {
115        for x in 0..w {
116            sorted.push((x, y, heightmap.get(x, y)));
117        }
118    }
119    sorted.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
120
121    // Flow accumulation: from high to low, add flow to downstream cell
122    for &(x, y, elev) in &sorted {
123        if elev < sea_level { continue; }
124        let (dx, dy) = flow_dir[y * w + x];
125        if dx == 0 && dy == 0 { continue; }
126
127        let nx = (x as i32 + dx).clamp(0, w as i32 - 1) as usize;
128        let ny = (y as i32 + dy).clamp(0, h as i32 - 1) as usize;
129        let upstream_flow = flow_grid.get(x, y);
130        flow_grid.add(nx, ny, upstream_flow);
131    }
132
133    // 3. Extract river segments above threshold
134    let threshold = compute_threshold(&flow_grid, sea_level, heightmap);
135    let mut segments = Vec::new();
136
137    for y in 0..h {
138        for x in 0..w {
139            let flow = flow_grid.get(x, y);
140            if flow > threshold && heightmap.get(x, y) > sea_level {
141                let (dx, dy) = flow_dir[y * w + x];
142                segments.push(RiverSegment {
143                    x,
144                    y,
145                    flow,
146                    width: (flow / threshold).sqrt().min(5.0),
147                    downstream: (dx, dy),
148                });
149            }
150        }
151    }
152
153    RiverNetwork { segments, flow_grid, threshold }
154}
155
156/// Compute a reasonable river threshold (top ~2% of flow values on land).
157fn compute_threshold(flow_grid: &Grid2D, sea_level: f32, heightmap: &Grid2D) -> f32 {
158    let mut land_flows: Vec<f32> = Vec::new();
159    for y in 0..flow_grid.height {
160        for x in 0..flow_grid.width {
161            if heightmap.get(x, y) > sea_level {
162                land_flows.push(flow_grid.get(x, y));
163            }
164        }
165    }
166    land_flows.sort_by(|a, b| a.partial_cmp(b).unwrap());
167    if land_flows.is_empty() { return 1.0; }
168    let idx = (land_flows.len() as f32 * 0.98) as usize;
169    land_flows[idx.min(land_flows.len() - 1)]
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_river_generation() {
178        // Simple slope: high on left, low on right
179        let mut hm = Grid2D::new(32, 32);
180        for y in 0..32 {
181            for x in 0..32 {
182                hm.set(x, y, 1.0 - x as f32 / 32.0);
183            }
184        }
185        let precip = Grid2D::filled(32, 32, 0.5);
186        let rn = generate(&hm, &precip, 0.1);
187        assert!(!rn.segments.is_empty(), "should generate rivers on a slope");
188    }
189
190    #[test]
191    fn test_flat_no_rivers() {
192        let hm = Grid2D::filled(16, 16, 0.5);
193        let precip = Grid2D::filled(16, 16, 0.5);
194        let rn = generate(&hm, &precip, 0.4);
195        // Flat terrain may not generate rivers (no gradient)
196        // This is acceptable behavior
197        assert!(rn.threshold > 0.0);
198    }
199}