Skip to main content

proof_engine/terrain/
mod.rs

1//! # Terrain System
2//!
3//! A complete terrain rendering and simulation system for the Proof Engine.
4//!
5//! ## Architecture
6//!
7//! The terrain system is organized into five submodules:
8//!
9//! - [`heightmap`] — Height field generation, erosion, analysis, and I/O
10//! - [`biome`]     — Climate simulation and biome classification
11//! - [`vegetation`] — Tree, grass, and rock placement with LOD
12//! - [`streaming`] — Async-style chunk loading, caching, and prefetching
13//! - [`mod_types`]  — Shared core data types (ChunkCoord, TerrainChunk, etc.)
14//!
15//! ## Quick Start
16//!
17//! ```rust,no_run
18//! use proof_engine::terrain::*;
19//!
20//! // Configure terrain generation
21//! let config = TerrainConfig::new(64, 8, 4, 12345);
22//!
23//! // Create the manager
24//! let mut manager = TerrainManager::new(config);
25//!
26//! // Update each frame with camera position
27//! manager.update(glam::Vec3::new(0.0, 50.0, 0.0));
28//!
29//! // Query terrain height
30//! let h = manager.sample_height(100.0, 200.0);
31//! println!("Height at (100, 200) = {h}");
32//! ```
33
34pub mod heightmap;
35pub mod biome;
36pub mod biomes;
37pub mod chunks;
38pub mod vegetation;
39pub mod streaming;
40pub mod mod_types;
41
42pub use mod_types::{ChunkCoord, ChunkState, TerrainConfig, TerrainChunk};
43pub use heightmap::{
44    HeightMap, DiamondSquare, FractalNoise, VoronoiPlates, PerlinTerrain,
45    HydraulicErosion, ThermalErosion, WindErosion,
46};
47pub use biome::{
48    BiomeType, BiomeParams, BiomeClassifier, BiomeMap, ClimateMap,
49    ClimateSimulator, VegetationDensity, BiomeColor, TransitionZone, SeasonFactor,
50};
51pub use vegetation::{
52    VegetationSystem, VegetationInstance, VegetationKind, VegetationLod,
53    TreeType, TreeParams, TreeSkeleton, TreeSegment, GrassCluster, GrassField,
54    RockPlacement, RockCluster, VegetationPainter, ImpostorBillboard,
55    generate_impostors,
56};
57pub use streaming::{
58    StreamingManager, ChunkCache, LoadQueue, ChunkGenerator, ChunkSerializer,
59    StreamingStats, VisibilitySet, LodScheduler, Prefetcher,
60};
61
62use glam::Vec3;
63
64// ── TerrainMaterial ───────────────────────────────────────────────────────────
65
66/// Material properties for terrain rendering.
67#[derive(Clone, Debug)]
68pub struct TerrainMaterial {
69    pub albedo:    Vec3,
70    pub normal:    Vec3,
71    pub roughness: f32,
72    pub layers:    Vec<TerrainLayer>,
73}
74
75impl Default for TerrainMaterial {
76    fn default() -> Self {
77        Self {
78            albedo:    Vec3::new(0.5, 0.45, 0.3),
79            normal:    Vec3::new(0.5, 1.0, 0.5),
80            roughness: 0.85,
81            layers:    Vec::new(),
82        }
83    }
84}
85
86impl TerrainMaterial {
87    pub fn new() -> Self { Self::default() }
88
89    /// Add a terrain layer.
90    pub fn add_layer(&mut self, layer: TerrainLayer) {
91        self.layers.push(layer);
92    }
93
94    /// Sample blended albedo based on altitude and slope.
95    pub fn sample_albedo(&self, altitude: f32, slope: f32) -> Vec3 {
96        if self.layers.is_empty() { return self.albedo; }
97        let mut result = Vec3::ZERO;
98        let mut total_weight = 0.0f32;
99        for layer in &self.layers {
100            let alt_blend = smooth_step(layer.blend_start, layer.blend_end, altitude);
101            let slope_ok = slope >= layer.slope_min && slope <= layer.slope_max;
102            let weight = alt_blend * if slope_ok { 1.0 } else { 0.0 };
103            result += layer.albedo * weight;
104            total_weight += weight;
105        }
106        if total_weight < 1e-6 { self.albedo } else { result / total_weight }
107    }
108}
109
110fn smooth_step(edge0: f32, edge1: f32, x: f32) -> f32 {
111    let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
112    t * t * (3.0 - 2.0 * t)
113}
114
115// ── TerrainLayer ──────────────────────────────────────────────────────────────
116
117/// A single material layer on terrain (e.g., grass, rock, snow).
118#[derive(Clone, Debug)]
119pub struct TerrainLayer {
120    pub name:          String,
121    pub albedo:        Vec3,
122    pub texture_scale: f32,
123    pub blend_start:   f32,
124    pub blend_end:     f32,
125    pub slope_min:     f32,
126    pub slope_max:     f32,
127    pub roughness:     f32,
128}
129
130impl TerrainLayer {
131    pub fn grass() -> Self {
132        Self {
133            name: "Grass".to_string(),
134            albedo: Vec3::new(0.3, 0.55, 0.15),
135            texture_scale: 4.0,
136            blend_start: 0.05,
137            blend_end: 0.5,
138            slope_min: 0.0,
139            slope_max: 0.4,
140            roughness: 0.9,
141        }
142    }
143
144    pub fn rock() -> Self {
145        Self {
146            name: "Rock".to_string(),
147            albedo: Vec3::new(0.5, 0.47, 0.44),
148            texture_scale: 2.0,
149            blend_start: 0.0,
150            blend_end: 1.0,
151            slope_min: 0.35,
152            slope_max: 1.0,
153            roughness: 0.75,
154        }
155    }
156
157    pub fn snow() -> Self {
158        Self {
159            name: "Snow".to_string(),
160            albedo: Vec3::new(0.9, 0.92, 0.95),
161            texture_scale: 3.0,
162            blend_start: 0.75,
163            blend_end: 0.9,
164            slope_min: 0.0,
165            slope_max: 0.6,
166            roughness: 0.3,
167        }
168    }
169
170    pub fn sand() -> Self {
171        Self {
172            name: "Sand".to_string(),
173            albedo: Vec3::new(0.85, 0.78, 0.55),
174            texture_scale: 5.0,
175            blend_start: 0.05,
176            blend_end: 0.15,
177            slope_min: 0.0,
178            slope_max: 0.2,
179            roughness: 0.95,
180        }
181    }
182}
183
184// ── TerrainCollider ───────────────────────────────────────────────────────────
185
186/// Height-field collision query interface.
187pub struct TerrainCollider<'a> {
188    heightmap:    &'a HeightMap,
189    chunk_size:   f32,
190    height_scale: f32,
191}
192
193impl<'a> TerrainCollider<'a> {
194    pub fn new(heightmap: &'a HeightMap, chunk_size: f32, height_scale: f32) -> Self {
195        Self { heightmap, chunk_size, height_scale }
196    }
197
198    pub fn height_at(&self, x: f32, z: f32) -> f32 {
199        let lx = (x / self.chunk_size * self.heightmap.width as f32)
200            .clamp(0.0, self.heightmap.width as f32 - 1.0);
201        let lz = (z / self.chunk_size * self.heightmap.height as f32)
202            .clamp(0.0, self.heightmap.height as f32 - 1.0);
203        self.heightmap.sample_bilinear(lx, lz) * self.height_scale
204    }
205
206    pub fn normal_at(&self, x: f32, z: f32) -> Vec3 {
207        let lx = (x / self.chunk_size * self.heightmap.width as f32) as usize;
208        let lz = (z / self.chunk_size * self.heightmap.height as f32) as usize;
209        self.heightmap.normal_at(
210            lx.min(self.heightmap.width  - 1),
211            lz.min(self.heightmap.height - 1),
212        )
213    }
214
215    pub fn is_below_surface(&self, x: f32, y: f32, z: f32) -> bool {
216        y < self.height_at(x, z)
217    }
218
219    /// Cast a ray against the heightfield; returns distance, or None.
220    pub fn ray_cast(&self, origin: Vec3, direction: Vec3, max_dist: f32) -> Option<f32> {
221        let dir = direction.normalize();
222        let step = self.chunk_size / self.heightmap.width as f32;
223        let mut t = 0.0f32;
224        let mut above = !self.is_below_surface(origin.x, origin.y, origin.z);
225        while t < max_dist {
226            let p = origin + dir * t;
227            let h = self.height_at(p.x, p.z);
228            let now_above = p.y > h;
229            if above && !now_above {
230                let mut lo = t - step;
231                let mut hi = t;
232                for _ in 0..8 {
233                    let mid = (lo + hi) * 0.5;
234                    let pm = origin + dir * mid;
235                    if pm.y > self.height_at(pm.x, pm.z) { lo = mid; } else { hi = mid; }
236                }
237                return Some((lo + hi) * 0.5);
238            }
239            above = now_above;
240            t += step;
241        }
242        None
243    }
244
245    /// Test if an AABB (center, half-extents) intersects the heightfield.
246    pub fn aabb_intersects(&self, center: Vec3, half_extents: Vec3) -> bool {
247        let corners = [
248            (center.x - half_extents.x, center.z - half_extents.z),
249            (center.x + half_extents.x, center.z - half_extents.z),
250            (center.x - half_extents.x, center.z + half_extents.z),
251            (center.x + half_extents.x, center.z + half_extents.z),
252            (center.x, center.z),
253        ];
254        let min_y = center.y - half_extents.y;
255        corners.iter().any(|&(x, z)| self.height_at(x, z) >= min_y)
256    }
257}
258
259// ── TerrainQuery ──────────────────────────────────────────────────────────────
260
261/// High-level query API for sampling terrain properties at world positions.
262pub struct TerrainQuery<'a> {
263    chunks: &'a mut StreamingManager,
264    config: TerrainConfig,
265}
266
267impl<'a> TerrainQuery<'a> {
268    pub fn new(chunks: &'a mut StreamingManager, config: TerrainConfig) -> Self {
269        Self { chunks, config }
270    }
271
272    pub fn sample_height(&mut self, x: f32, z: f32) -> f32 {
273        self.chunks.sample_height_world(x, z)
274    }
275
276    pub fn sample_normal(&mut self, x: f32, z: f32) -> Vec3 {
277        let chunk_world = self.config.chunk_size as f32;
278        let cx = (x / chunk_world).floor() as i32;
279        let cz = (z / chunk_world).floor() as i32;
280        let coord = ChunkCoord(cx, cz);
281        let lx = (x - cx as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32;
282        let lz = (z - cz as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32;
283        if let Some(chunk) = self.chunks.get_chunk(coord) {
284            let xi = (lx as usize).min(chunk.heightmap.width  - 1);
285            let zi = (lz as usize).min(chunk.heightmap.height - 1);
286            chunk.heightmap.normal_at(xi, zi)
287        } else {
288            Vec3::Y
289        }
290    }
291
292    pub fn get_biome(&mut self, x: f32, z: f32) -> BiomeType {
293        let chunk_world = self.config.chunk_size as f32;
294        let cx = (x / chunk_world).floor() as i32;
295        let cz = (z / chunk_world).floor() as i32;
296        let coord = ChunkCoord(cx, cz);
297        let lx = ((x - cx as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32) as usize;
298        let lz = ((z - cz as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32) as usize;
299        if let Some(chunk) = self.chunks.get_chunk(coord) {
300            if let Some(ref bm) = chunk.biome_map {
301                return bm.get(lx.min(bm.width - 1), lz.min(bm.height - 1));
302            }
303        }
304        BiomeType::Grassland
305    }
306
307    pub fn is_underwater(&mut self, x: f32, z: f32) -> bool {
308        self.chunks.sample_height_world(x, z) < 0.1
309    }
310}
311
312// ── TerrainManager ────────────────────────────────────────────────────────────
313
314/// Top-level terrain system coordinator.
315pub struct TerrainManager {
316    pub config:        TerrainConfig,
317    pub streaming:     StreamingManager,
318    pub material:      TerrainMaterial,
319    camera_pos:        Vec3,
320    current_month:     u32,
321}
322
323impl TerrainManager {
324    pub fn new(config: TerrainConfig) -> Self {
325        let streaming = StreamingManager::new(config.clone());
326        Self {
327            streaming,
328            material: Self::default_material(),
329            config,
330            camera_pos: Vec3::ZERO,
331            current_month: 0,
332        }
333    }
334
335    pub fn new_synchronous(config: TerrainConfig) -> Self {
336        let streaming = StreamingManager::new_synchronous(config.clone());
337        Self {
338            streaming,
339            material: Self::default_material(),
340            config,
341            camera_pos: Vec3::ZERO,
342            current_month: 0,
343        }
344    }
345
346    fn default_material() -> TerrainMaterial {
347        let mut mat = TerrainMaterial::new();
348        mat.add_layer(TerrainLayer::sand());
349        mat.add_layer(TerrainLayer::grass());
350        mat.add_layer(TerrainLayer::rock());
351        mat.add_layer(TerrainLayer::snow());
352        mat
353    }
354
355    pub fn update(&mut self, camera_pos: Vec3) {
356        self.camera_pos = camera_pos;
357        self.streaming.update(camera_pos);
358    }
359
360    pub fn set_month(&mut self, month: u32) {
361        self.current_month = month % 12;
362    }
363
364    pub fn sample_height(&mut self, x: f32, z: f32) -> f32 {
365        self.streaming.sample_height_world(x, z)
366    }
367
368    pub fn sample_normal(&mut self, x: f32, z: f32) -> Vec3 {
369        let chunk_world = self.config.chunk_size as f32;
370        let cx = (x / chunk_world).floor() as i32;
371        let cz = (z / chunk_world).floor() as i32;
372        let coord = ChunkCoord(cx, cz);
373        let lx = ((x - cx as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32) as usize;
374        let lz = ((z - cz as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32) as usize;
375        if let Some(chunk) = self.streaming.get_chunk(coord) {
376            let xi = lx.min(chunk.heightmap.width  - 1);
377            let zi = lz.min(chunk.heightmap.height - 1);
378            chunk.heightmap.normal_at(xi, zi)
379        } else {
380            Vec3::Y
381        }
382    }
383
384    pub fn get_biome(&mut self, x: f32, z: f32) -> BiomeType {
385        let chunk_world = self.config.chunk_size as f32;
386        let cx = (x / chunk_world).floor() as i32;
387        let cz = (z / chunk_world).floor() as i32;
388        let coord = ChunkCoord(cx, cz);
389        let lx = ((x - cx as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32) as usize;
390        let lz = ((z - cz as f32 * chunk_world) / chunk_world * self.config.chunk_size as f32) as usize;
391        if let Some(chunk) = self.streaming.get_chunk(coord) {
392            if let Some(ref bm) = chunk.biome_map {
393                return bm.get(lx.min(bm.width - 1), lz.min(bm.height - 1));
394            }
395        }
396        BiomeType::Grassland
397    }
398
399    pub fn is_underwater(&mut self, x: f32, z: f32) -> bool {
400        self.sample_height(x, z) < 0.1
401    }
402
403    pub fn stats(&self) -> &StreamingStats { self.streaming.stats() }
404    pub fn loaded_chunk_count(&self) -> usize { self.streaming.cache_size() }
405
406    pub fn ensure_loaded(&mut self, x: f32, z: f32) {
407        let chunk_world = self.config.chunk_size as f32;
408        let coord = ChunkCoord(
409            (x / chunk_world).floor() as i32,
410            (z / chunk_world).floor() as i32,
411        );
412        self.streaming.force_load(coord);
413    }
414
415    pub fn collider_for_chunk<'a>(chunk: &'a TerrainChunk, config: &TerrainConfig) -> TerrainCollider<'a> {
416        TerrainCollider::new(&chunk.heightmap, config.chunk_size as f32, 100.0)
417    }
418}
419
420// ── Tests ─────────────────────────────────────────────────────────────────────
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    fn simple_config() -> TerrainConfig {
427        TerrainConfig { chunk_size: 16, view_distance: 1, lod_levels: 2, seed: 42 }
428    }
429
430    #[test]
431    fn test_chunk_coord_neighbors() {
432        let c = ChunkCoord(0, 0);
433        let n4 = c.neighbors_4();
434        assert!(n4.contains(&ChunkCoord(-1, 0)));
435        assert!(n4.contains(&ChunkCoord(1,  0)));
436        assert!(n4.contains(&ChunkCoord(0, -1)));
437        assert!(n4.contains(&ChunkCoord(0,  1)));
438    }
439
440    #[test]
441    fn test_chunk_coord_chebyshev() {
442        assert_eq!(ChunkCoord(0, 0).chebyshev_distance(ChunkCoord(3, 2)), 3);
443        assert_eq!(ChunkCoord(0, 0).chebyshev_distance(ChunkCoord(0, 0)), 0);
444    }
445
446    #[test]
447    fn test_chunk_coord_euclidean() {
448        let d = ChunkCoord(0, 0).euclidean_distance(ChunkCoord(3, 4));
449        assert!((d - 5.0).abs() < 1e-4);
450    }
451
452    #[test]
453    fn test_chunk_coord_world_pos() {
454        let c = ChunkCoord(2, 3);
455        let p = c.to_world_pos(64.0);
456        assert!((p.x - 160.0).abs() < 1e-4);
457        assert!((p.z - 224.0).abs() < 1e-4);
458    }
459
460    #[test]
461    fn test_chunk_coord_from_world_pos() {
462        let p = Vec3::new(130.0, 0.0, 200.0);
463        let c = ChunkCoord::from_world_pos(p, 64.0);
464        assert_eq!(c, ChunkCoord(2, 3));
465    }
466
467    #[test]
468    fn test_chunk_coord_within_radius() {
469        assert!( ChunkCoord(0, 0).within_radius(ChunkCoord(2, 2), 3));
470        assert!(!ChunkCoord(0, 0).within_radius(ChunkCoord(5, 0), 3));
471    }
472
473    #[test]
474    fn test_terrain_material_sample_albedo() {
475        let mut mat = TerrainMaterial::new();
476        mat.add_layer(TerrainLayer::grass());
477        mat.add_layer(TerrainLayer::snow());
478        let snow_color  = mat.sample_albedo(0.9, 0.1);
479        let grass_color = mat.sample_albedo(0.2, 0.1);
480        assert!(snow_color.x > grass_color.x || snow_color.y > grass_color.y);
481    }
482
483    #[test]
484    fn test_terrain_collider_height_at() {
485        let mut hm = HeightMap::new(64, 64);
486        for i in 0..(64 * 64) { hm.data[i] = 0.5; }
487        let col = TerrainCollider::new(&hm, 64.0, 100.0);
488        let h = col.height_at(32.0, 32.0);
489        assert!((h - 50.0).abs() < 1.0);
490    }
491
492    #[test]
493    fn test_terrain_collider_is_below() {
494        let mut hm = HeightMap::new(64, 64);
495        for i in 0..(64 * 64) { hm.data[i] = 0.5; }
496        let col = TerrainCollider::new(&hm, 64.0, 100.0);
497        assert!( col.is_below_surface(32.0, 10.0, 32.0));
498        assert!(!col.is_below_surface(32.0, 80.0, 32.0));
499    }
500
501    #[test]
502    fn test_terrain_collider_ray_cast() {
503        let mut hm = HeightMap::new(64, 64);
504        for i in 0..(64 * 64) { hm.data[i] = 0.5; }
505        let col = TerrainCollider::new(&hm, 64.0, 100.0);
506        let hit = col.ray_cast(
507            Vec3::new(32.0, 200.0, 32.0),
508            Vec3::new(0.0, -1.0, 0.0),
509            300.0,
510        );
511        assert!(hit.is_some(), "Ray should hit flat terrain");
512        let dist = hit.unwrap();
513        assert!((dist - 150.0).abs() < 5.0);
514    }
515
516    #[test]
517    fn test_terrain_collider_aabb() {
518        let mut hm = HeightMap::new(64, 64);
519        for i in 0..(64 * 64) { hm.data[i] = 0.5; }
520        let col = TerrainCollider::new(&hm, 64.0, 100.0);
521        assert!( col.aabb_intersects(Vec3::new(32.0,  50.0, 32.0), Vec3::new(5.0, 5.0, 5.0)));
522        assert!(!col.aabb_intersects(Vec3::new(32.0, 500.0, 32.0), Vec3::new(5.0, 5.0, 5.0)));
523    }
524
525    #[test]
526    fn test_terrain_manager_creation() {
527        let config = simple_config();
528        let manager = TerrainManager::new_synchronous(config);
529        assert_eq!(manager.loaded_chunk_count(), 0);
530    }
531
532    #[test]
533    fn test_terrain_manager_update() {
534        let config = simple_config();
535        let mut manager = TerrainManager::new_synchronous(config);
536        manager.update(Vec3::new(0.0, 50.0, 0.0));
537        let _s = manager.stats();
538    }
539
540    #[test]
541    fn test_terrain_manager_ensure_loaded() {
542        let config = simple_config();
543        let mut manager = TerrainManager::new_synchronous(config);
544        manager.ensure_loaded(0.0, 0.0);
545        assert_eq!(manager.loaded_chunk_count(), 1);
546    }
547
548    #[test]
549    fn test_terrain_layers_valid() {
550        for layer in &[TerrainLayer::sand(), TerrainLayer::grass(), TerrainLayer::rock(), TerrainLayer::snow()] {
551            assert!(layer.albedo.x >= 0.0 && layer.albedo.x <= 1.0);
552            assert!(layer.albedo.y >= 0.0 && layer.albedo.y <= 1.0);
553            assert!(layer.albedo.z >= 0.0 && layer.albedo.z <= 1.0);
554            assert!(layer.blend_start <= layer.blend_end);
555        }
556    }
557}
558
559// ── Terrain Painter ───────────────────────────────────────────────────────────
560
561/// A brush tool for real-time terrain sculpting.
562#[derive(Clone, Debug)]
563pub struct TerrainPainter {
564    pub brush_radius:   f32,
565    pub brush_strength: f32,
566    pub brush_falloff:  BrushFalloff,
567    pub mode:           PaintMode,
568}
569
570/// Brush falloff shape.
571#[derive(Clone, Copy, Debug)]
572pub enum BrushFalloff {
573    Linear,
574    Smooth,
575    Constant,
576    Gaussian,
577}
578
579/// What operation the terrain painter performs.
580#[derive(Clone, Copy, Debug)]
581pub enum PaintMode {
582    Raise,
583    Lower,
584    Flatten { target: f32 },
585    Smooth,
586    Noise { seed: u64, scale: f32 },
587}
588
589impl TerrainPainter {
590    pub fn new(radius: f32, strength: f32) -> Self {
591        Self {
592            brush_radius: radius,
593            brush_strength: strength,
594            brush_falloff: BrushFalloff::Smooth,
595            mode: PaintMode::Raise,
596        }
597    }
598
599    fn falloff(&self, dist_normalized: f32) -> f32 {
600        match self.brush_falloff {
601            BrushFalloff::Linear    => (1.0 - dist_normalized).max(0.0),
602            BrushFalloff::Smooth    => {
603                let t = (1.0 - dist_normalized).clamp(0.0, 1.0);
604                t * t * (3.0 - 2.0 * t)
605            }
606            BrushFalloff::Constant  => if dist_normalized < 1.0 { 1.0 } else { 0.0 },
607            BrushFalloff::Gaussian  => {
608                let sigma = 0.4f32;
609                (-(dist_normalized * dist_normalized) / (2.0 * sigma * sigma)).exp()
610            }
611        }
612    }
613
614    /// Apply brush at world position (cx, cz) to a heightmap.
615    pub fn apply(&self, hm: &mut HeightMap, cx: f32, cz: f32) {
616        let r = self.brush_radius;
617        let x0 = ((cx - r).floor() as i32).max(0) as usize;
618        let z0 = ((cz - r).floor() as i32).max(0) as usize;
619        let x1 = ((cx + r).ceil()  as i32).min(hm.width  as i32 - 1) as usize;
620        let z1 = ((cz + r).ceil()  as i32).min(hm.height as i32 - 1) as usize;
621
622        for z in z0..=z1 {
623            for x in x0..=x1 {
624                let dx = x as f32 - cx;
625                let dz = z as f32 - cz;
626                let dist = (dx * dx + dz * dz).sqrt();
627                if dist >= r { continue; }
628                let falloff = self.falloff(dist / r);
629                let delta = self.brush_strength * falloff;
630                let cur = hm.get(x, z);
631                let new_val = match self.mode {
632                    PaintMode::Raise         => cur + delta,
633                    PaintMode::Lower         => cur - delta,
634                    PaintMode::Flatten { target } => cur + (target - cur) * delta,
635                    PaintMode::Smooth        => {
636                        let n = hm.normal_at(x, z);
637                        // Smooth toward neighborhood average
638                        let neighbors = [
639                            hm.get(x.saturating_sub(1), z),
640                            hm.get((x+1).min(hm.width-1), z),
641                            hm.get(x, z.saturating_sub(1)),
642                            hm.get(x, (z+1).min(hm.height-1)),
643                        ];
644                        let avg = neighbors.iter().sum::<f32>() / 4.0;
645                        cur + (avg - cur) * delta
646                    }
647                    PaintMode::Noise { seed, scale } => {
648                        let noise = heightmap::GradientNoisePublic::new(seed);
649                        let n = noise.noise2d(x as f32 * scale, z as f32 * scale);
650                        cur + (n * 2.0 - 1.0) * delta
651                    }
652                };
653                hm.set(x, z, new_val.clamp(0.0, 1.0));
654            }
655        }
656    }
657
658    /// Compute brush preview: returns a list of (x, z, intensity) samples.
659    pub fn preview_samples(&self, cx: f32, cz: f32, sample_count: usize) -> Vec<(f32, f32, f32)> {
660        let mut samples = Vec::new();
661        let r = self.brush_radius;
662        for i in 0..sample_count {
663            let angle = i as f32 * std::f32::consts::TAU / sample_count as f32;
664            for dist_step in 0..=4 {
665                let dist = r * dist_step as f32 / 4.0;
666                let x = cx + angle.cos() * dist;
667                let z = cz + angle.sin() * dist;
668                let intensity = self.falloff(dist / r.max(0.001));
669                samples.push((x, z, intensity));
670            }
671        }
672        samples
673    }
674}
675
676// ── Terrain Heightmap Builder ─────────────────────────────────────────────────
677
678/// A builder for compositing multiple terrain generation steps.
679pub struct TerrainHeightmapBuilder {
680    width:    usize,
681    height:   usize,
682    steps:    Vec<BuildStep>,
683}
684
685enum BuildStep {
686    Diamond   { roughness: f32, seed: u64 },
687    Fractal   { octaves: usize, lacunarity: f32, persistence: f32, scale: f32, seed: u64 },
688    Voronoi   { num_plates: usize, seed: u64 },
689    Perlin    { octaves: usize, scale: f32, seed: u64 },
690    Erode     { kind: ErosionKind, iterations: usize },
691    Terrace   { levels: usize },
692    IslandMask{ falloff: f32 },
693    Normalize,
694    Blur      { radius: usize },
695    Sharpen   { amount: f32 },
696}
697
698enum ErosionKind {
699    Hydraulic { rain: f32, capacity: f32, evaporation: f32, seed: u64 },
700    Thermal   { talus: f32 },
701    Wind      { dir: glam::Vec2 },
702}
703
704impl TerrainHeightmapBuilder {
705    pub fn new(width: usize, height: usize) -> Self {
706        Self { width, height, steps: Vec::new() }
707    }
708
709    pub fn diamond_square(mut self, roughness: f32, seed: u64) -> Self {
710        self.steps.push(BuildStep::Diamond { roughness, seed });
711        self
712    }
713
714    pub fn fractal_noise(mut self, octaves: usize, lacunarity: f32, persistence: f32, scale: f32, seed: u64) -> Self {
715        self.steps.push(BuildStep::Fractal { octaves, lacunarity, persistence, scale, seed });
716        self
717    }
718
719    pub fn voronoi_plates(mut self, num_plates: usize, seed: u64) -> Self {
720        self.steps.push(BuildStep::Voronoi { num_plates, seed });
721        self
722    }
723
724    pub fn perlin(mut self, octaves: usize, scale: f32, seed: u64) -> Self {
725        self.steps.push(BuildStep::Perlin { octaves, scale, seed });
726        self
727    }
728
729    pub fn hydraulic_erosion(mut self, iterations: usize, rain: f32, capacity: f32, evap: f32, seed: u64) -> Self {
730        self.steps.push(BuildStep::Erode { kind: ErosionKind::Hydraulic { rain, capacity, evaporation: evap, seed }, iterations });
731        self
732    }
733
734    pub fn thermal_erosion(mut self, iterations: usize, talus: f32) -> Self {
735        self.steps.push(BuildStep::Erode { kind: ErosionKind::Thermal { talus }, iterations });
736        self
737    }
738
739    pub fn wind_erosion(mut self, iterations: usize, dir: glam::Vec2) -> Self {
740        self.steps.push(BuildStep::Erode { kind: ErosionKind::Wind { dir }, iterations });
741        self
742    }
743
744    pub fn terrace(mut self, levels: usize) -> Self {
745        self.steps.push(BuildStep::Terrace { levels });
746        self
747    }
748
749    pub fn island_mask(mut self, falloff: f32) -> Self {
750        self.steps.push(BuildStep::IslandMask { falloff });
751        self
752    }
753
754    pub fn normalize(mut self) -> Self {
755        self.steps.push(BuildStep::Normalize);
756        self
757    }
758
759    pub fn blur(mut self, radius: usize) -> Self {
760        self.steps.push(BuildStep::Blur { radius });
761        self
762    }
763
764    pub fn sharpen(mut self, amount: f32) -> Self {
765        self.steps.push(BuildStep::Sharpen { amount });
766        self
767    }
768
769    /// Execute the build pipeline and return the resulting heightmap.
770    pub fn build(self) -> HeightMap {
771        let mut hm: Option<HeightMap> = None;
772        let w = self.width;
773        let h = self.height;
774
775        for step in self.steps {
776            match step {
777                BuildStep::Diamond { roughness, seed } => {
778                    let size = w.max(h).next_power_of_two();
779                    let generated = DiamondSquare::generate(size, roughness, seed);
780                    let resampled = generated.resample(w, h);
781                    hm = Some(Self::merge(hm, resampled));
782                }
783                BuildStep::Fractal { octaves, lacunarity, persistence, scale, seed } => {
784                    let generated = FractalNoise::generate(w, h, octaves, lacunarity, persistence, scale, seed);
785                    hm = Some(Self::merge(hm, generated));
786                }
787                BuildStep::Voronoi { num_plates, seed } => {
788                    let generated = VoronoiPlates::generate(w, h, num_plates, seed);
789                    hm = Some(Self::merge(hm, generated));
790                }
791                BuildStep::Perlin { octaves, scale, seed } => {
792                    let generated = PerlinTerrain::generate(w, h, octaves, scale, seed);
793                    hm = Some(Self::merge(hm, generated));
794                }
795                BuildStep::Erode { kind, iterations } => {
796                    if let Some(ref mut m) = hm {
797                        match kind {
798                            ErosionKind::Hydraulic { rain, capacity, evaporation, seed } => {
799                                HydraulicErosion::erode(m, iterations, rain, capacity, evaporation, seed);
800                            }
801                            ErosionKind::Thermal { talus } => {
802                                ThermalErosion::erode(m, iterations, talus);
803                            }
804                            ErosionKind::Wind { dir } => {
805                                WindErosion::erode(m, dir, iterations);
806                            }
807                        }
808                    }
809                }
810                BuildStep::Terrace { levels } => {
811                    if let Some(ref mut m) = hm { m.terrace(levels); }
812                }
813                BuildStep::IslandMask { falloff } => {
814                    if let Some(ref mut m) = hm { m.island_mask(falloff); }
815                }
816                BuildStep::Normalize => {
817                    if let Some(ref mut m) = hm { m.normalize(); }
818                }
819                BuildStep::Blur { radius } => {
820                    if let Some(ref mut m) = hm { m.blur(radius); }
821                }
822                BuildStep::Sharpen { amount } => {
823                    if let Some(ref mut m) = hm { m.sharpen(amount); }
824                }
825            }
826        }
827
828        hm.unwrap_or_else(|| HeightMap::new(w, h))
829    }
830
831    /// Merge two heightmaps (average if both exist, use new if only new exists).
832    fn merge(existing: Option<HeightMap>, new: HeightMap) -> HeightMap {
833        match existing {
834            None => new,
835            Some(mut e) => {
836                for (a, &b) in e.data.iter_mut().zip(new.data.iter()) {
837                    *a = (*a + b) * 0.5;
838                }
839                e
840            }
841        }
842    }
843}
844
845// (HeightMap::resample is defined in heightmap.rs)
846
847// ── Terrain LOD System ────────────────────────────────────────────────────────
848
849/// Parameters for heightmap LOD (level-of-detail) scaling.
850#[derive(Clone, Debug)]
851pub struct TerrainLodParams {
852    /// Number of LOD levels.
853    pub num_levels: usize,
854    /// Distance threshold per level (in world units).
855    pub thresholds: Vec<f32>,
856    /// Resolution divisor per level (1 = full, 2 = half, 4 = quarter, …).
857    pub divisors:   Vec<usize>,
858}
859
860impl TerrainLodParams {
861    pub fn new(num_levels: usize, base_threshold: f32, chunk_size: f32) -> Self {
862        let thresholds: Vec<f32> = (0..num_levels)
863            .map(|l| base_threshold * (1 << l) as f32)
864            .collect();
865        let divisors: Vec<usize> = (0..num_levels)
866            .map(|l| 1 << l)
867            .collect();
868        Self { num_levels, thresholds, divisors }
869    }
870
871    /// Determine LOD level for a given distance.
872    pub fn lod_for_distance(&self, dist: f32) -> usize {
873        for (l, &thresh) in self.thresholds.iter().enumerate() {
874            if dist < thresh { return l; }
875        }
876        self.num_levels - 1
877    }
878
879    /// Resolution for a given LOD level and base chunk size.
880    pub fn resolution(&self, lod: usize, base_size: usize) -> usize {
881        let div = self.divisors.get(lod).copied().unwrap_or(1 << (self.num_levels - 1));
882        (base_size / div).max(4)
883    }
884}
885
886// ── Terrain Metadata ──────────────────────────────────────────────────────────
887
888/// Metadata about a generated terrain world.
889#[derive(Clone, Debug)]
890pub struct TerrainMetadata {
891    pub seed:            u64,
892    pub world_name:      String,
893    pub generation_time: f32,
894    pub total_chunks:    usize,
895    pub sea_level:       f32,
896    pub max_height:      f32,
897    pub land_fraction:   f32,
898    pub biome_counts:    [usize; 20],
899}
900
901impl TerrainMetadata {
902    pub fn new(seed: u64, world_name: &str) -> Self {
903        Self {
904            seed,
905            world_name: world_name.to_string(),
906            generation_time: 0.0,
907            total_chunks: 0,
908            sea_level: 0.1,
909            max_height: 100.0,
910            land_fraction: 0.0,
911            biome_counts: [0usize; 20],
912        }
913    }
914
915    /// Compute land fraction from a world overview heightmap.
916    pub fn compute_land_fraction(hm: &HeightMap, sea_level: f32) -> f32 {
917        let land = hm.data.iter().filter(|&&v| v > sea_level).count();
918        land as f32 / hm.data.len() as f32
919    }
920}
921
922// ── Terrain Raycast System ────────────────────────────────────────────────────
923
924/// System for batched terrain raycasts.
925pub struct TerrainRaycastSystem;
926
927impl TerrainRaycastSystem {
928    /// Cast multiple rays against a heightmap. Returns vec of (hit_dist, hit_pos) or None per ray.
929    pub fn batch_raycast(
930        hm:     &HeightMap,
931        chunk_size: f32,
932        height_scale: f32,
933        rays:   &[(Vec3, Vec3)],  // (origin, direction) pairs
934        max_dist: f32,
935    ) -> Vec<Option<(f32, Vec3)>> {
936        let collider = TerrainCollider::new(hm, chunk_size, height_scale);
937        rays.iter().map(|&(origin, dir)| {
938            collider.ray_cast(origin, dir, max_dist)
939                .map(|d| (d, origin + dir.normalize() * d))
940        }).collect()
941    }
942
943    /// Find the first ray that hits terrain. Returns index and hit info.
944    pub fn first_hit(
945        hm:     &HeightMap,
946        chunk_size: f32,
947        height_scale: f32,
948        rays:   &[(Vec3, Vec3)],
949        max_dist: f32,
950    ) -> Option<(usize, f32, Vec3)> {
951        let collider = TerrainCollider::new(hm, chunk_size, height_scale);
952        for (i, &(origin, dir)) in rays.iter().enumerate() {
953            if let Some(d) = collider.ray_cast(origin, dir, max_dist) {
954                return Some((i, d, origin + dir.normalize() * d));
955            }
956        }
957        None
958    }
959}
960
961// ── Terrain Water ─────────────────────────────────────────────────────────────
962
963/// Represents bodies of water on the terrain.
964#[derive(Clone, Debug)]
965pub struct TerrainWater {
966    pub sea_level:    f32,   // normalized height of sea level
967    pub river_width:  f32,
968    /// Precomputed water mask (1 = water, 0 = land).
969    pub water_mask:   HeightMap,
970}
971
972impl TerrainWater {
973    pub fn new(heightmap: &HeightMap, sea_level: f32) -> Self {
974        let mut water_mask = HeightMap::new(heightmap.width, heightmap.height);
975        for (i, &h) in heightmap.data.iter().enumerate() {
976            water_mask.data[i] = if h <= sea_level { 1.0 } else { 0.0 };
977        }
978        Self { sea_level, river_width: 2.0, water_mask }
979    }
980
981    /// Is a world position underwater?
982    pub fn is_underwater(&self, x: f32, z: f32) -> bool {
983        let lx = x.clamp(0.0, (self.water_mask.width  - 1) as f32);
984        let lz = z.clamp(0.0, (self.water_mask.height - 1) as f32);
985        self.water_mask.sample_bilinear(lx, lz) > 0.5
986    }
987
988    /// Fraction of the terrain covered by water.
989    pub fn water_coverage(&self) -> f32 {
990        self.water_mask.data.iter().filter(|&&v| v > 0.5).count() as f32
991            / self.water_mask.data.len() as f32
992    }
993
994    /// Depth below sea level at a given normalized height.
995    pub fn depth(&self, height: f32) -> f32 {
996        (self.sea_level - height).max(0.0)
997    }
998}
999
1000// ── Extended mod.rs Tests ─────────────────────────────────────────────────────
1001
1002#[cfg(test)]
1003mod extended_mod_tests {
1004    use super::*;
1005
1006    #[test]
1007    fn test_terrain_painter_raise() {
1008        let mut hm = HeightMap::new(64, 64);
1009        let painter = TerrainPainter {
1010            brush_radius: 10.0,
1011            brush_strength: 0.5,
1012            brush_falloff: BrushFalloff::Smooth,
1013            mode: PaintMode::Raise,
1014        };
1015        painter.apply(&mut hm, 32.0, 32.0);
1016        assert!(hm.get(32, 32) > 0.0);
1017        assert_eq!(hm.get(0, 0), 0.0);
1018    }
1019
1020    #[test]
1021    fn test_terrain_painter_lower() {
1022        let mut hm = HeightMap::new(64, 64);
1023        for v in hm.data.iter_mut() { *v = 0.5; }
1024        let painter = TerrainPainter {
1025            brush_radius: 10.0,
1026            brush_strength: 0.3,
1027            brush_falloff: BrushFalloff::Linear,
1028            mode: PaintMode::Lower,
1029        };
1030        painter.apply(&mut hm, 32.0, 32.0);
1031        assert!(hm.get(32, 32) < 0.5);
1032    }
1033
1034    #[test]
1035    fn test_terrain_painter_flatten() {
1036        let mut hm = HeightMap::new(64, 64);
1037        for v in hm.data.iter_mut() { *v = 0.8; }
1038        let painter = TerrainPainter {
1039            brush_radius: 20.0,
1040            brush_strength: 1.0,
1041            brush_falloff: BrushFalloff::Constant,
1042            mode: PaintMode::Flatten { target: 0.4 },
1043        };
1044        painter.apply(&mut hm, 32.0, 32.0);
1045        // Center should be close to target
1046        assert!((hm.get(32, 32) - 0.4).abs() < 0.05);
1047    }
1048
1049    #[test]
1050    fn test_terrain_heightmap_builder() {
1051        let hm = TerrainHeightmapBuilder::new(32, 32)
1052            .fractal_noise(4, 2.0, 0.5, 3.0, 42)
1053            .normalize()
1054            .blur(1)
1055            .build();
1056        assert_eq!(hm.width, 32);
1057        assert_eq!(hm.height, 32);
1058        let mn = hm.min_value();
1059        let mx = hm.max_value();
1060        assert!(mn >= 0.0 && mx <= 1.0);
1061    }
1062
1063    #[test]
1064    fn test_terrain_heightmap_builder_multi_step() {
1065        let hm = TerrainHeightmapBuilder::new(32, 32)
1066            .fractal_noise(4, 2.0, 0.5, 3.0, 42)
1067            .island_mask(2.0)
1068            .normalize()
1069            .terrace(4)
1070            .build();
1071        assert_eq!(hm.data.len(), 32 * 32);
1072    }
1073
1074    #[test]
1075    fn test_terrain_lod_params() {
1076        let lod = TerrainLodParams::new(4, 50.0, 64.0);
1077        assert_eq!(lod.lod_for_distance(10.0),   0);
1078        assert_eq!(lod.lod_for_distance(60.0),   1);
1079        assert_eq!(lod.lod_for_distance(120.0),  2);
1080        assert_eq!(lod.resolution(0, 64),        64);
1081        assert_eq!(lod.resolution(1, 64),        32);
1082        assert_eq!(lod.resolution(2, 64),        16);
1083    }
1084
1085    #[test]
1086    fn test_terrain_water() {
1087        let hm = heightmap::FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1088        let water = TerrainWater::new(&hm, 0.15);
1089        let coverage = water.water_coverage();
1090        assert!(coverage >= 0.0 && coverage <= 1.0);
1091    }
1092
1093    #[test]
1094    fn test_terrain_metadata() {
1095        let hm = heightmap::FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1096        let frac = TerrainMetadata::compute_land_fraction(&hm, 0.15);
1097        assert!(frac >= 0.0 && frac <= 1.0);
1098    }
1099
1100    #[test]
1101    fn test_terrain_raycast_system_batch() {
1102        let mut hm = HeightMap::new(64, 64);
1103        for v in hm.data.iter_mut() { *v = 0.5; }
1104        let rays = vec![
1105            (Vec3::new(32.0, 200.0, 32.0), Vec3::new(0.0, -1.0, 0.0)),
1106            (Vec3::new(10.0, 200.0, 10.0), Vec3::new(0.0, -1.0, 0.0)),
1107        ];
1108        let results = TerrainRaycastSystem::batch_raycast(&hm, 64.0, 100.0, &rays, 300.0);
1109        assert_eq!(results.len(), 2);
1110        assert!(results[0].is_some());
1111    }
1112
1113    #[test]
1114    fn test_smooth_step() {
1115        assert!((smooth_step(0.0, 1.0, 0.0) - 0.0).abs() < 1e-5);
1116        assert!((smooth_step(0.0, 1.0, 1.0) - 1.0).abs() < 1e-5);
1117        assert!((smooth_step(0.0, 1.0, 0.5) - 0.5).abs() < 1e-5);
1118    }
1119}
1120
1121// ─────────────────────────────────────────────────────────────────────────────
1122// Terrain event system
1123// ─────────────────────────────────────────────────────────────────────────────
1124
1125/// Discrete events that the terrain system can emit during simulation.
1126#[derive(Debug, Clone)]
1127pub enum TerrainEvent {
1128    ChunkLoaded { coord: mod_types::ChunkCoord },
1129    ChunkUnloaded { coord: mod_types::ChunkCoord },
1130    HeightmapModified { coord: mod_types::ChunkCoord, affected_cells: u32 },
1131    BiomeTransitionDetected { from: biome::BiomeType, to: biome::BiomeType },
1132    ErosionCycleCompleted { chunk: mod_types::ChunkCoord, delta_energy: f32 },
1133    WaterLevelChanged { old: f32, new: f32 },
1134    LodChanged { coord: mod_types::ChunkCoord, old_lod: u8, new_lod: u8 },
1135}
1136
1137/// Simple single-producer/single-consumer event queue for terrain events.
1138#[derive(Debug, Default)]
1139pub struct TerrainEventQueue {
1140    events: std::collections::VecDeque<TerrainEvent>,
1141    pub max_capacity: usize,
1142}
1143
1144impl TerrainEventQueue {
1145    pub fn new(capacity: usize) -> Self {
1146        Self { events: std::collections::VecDeque::new(), max_capacity: capacity }
1147    }
1148
1149    pub fn push(&mut self, event: TerrainEvent) {
1150        if self.events.len() >= self.max_capacity {
1151            self.events.pop_front();  // drop oldest
1152        }
1153        self.events.push_back(event);
1154    }
1155
1156    pub fn pop(&mut self) -> Option<TerrainEvent> {
1157        self.events.pop_front()
1158    }
1159
1160    pub fn drain_all(&mut self) -> Vec<TerrainEvent> {
1161        self.events.drain(..).collect()
1162    }
1163
1164    pub fn len(&self) -> usize { self.events.len() }
1165    pub fn is_empty(&self) -> bool { self.events.is_empty() }
1166}
1167
1168// ─────────────────────────────────────────────────────────────────────────────
1169// Terrain snapshot / diff
1170// ─────────────────────────────────────────────────────────────────────────────
1171
1172/// Immutable snapshot of a heightmap for undo/redo support.
1173#[derive(Debug, Clone)]
1174pub struct TerrainSnapshot {
1175    pub width: usize,
1176    pub height: usize,
1177    data: Vec<f32>,
1178    pub timestamp: u64,
1179}
1180
1181impl TerrainSnapshot {
1182    pub fn capture(hm: &heightmap::HeightMap, timestamp: u64) -> Self {
1183        Self { width: hm.width, height: hm.height, data: hm.data.clone(), timestamp }
1184    }
1185
1186    pub fn restore_to(&self, hm: &mut heightmap::HeightMap) {
1187        if hm.width == self.width && hm.height == self.height {
1188            hm.data.copy_from_slice(&self.data);
1189        }
1190    }
1191
1192    pub fn byte_size(&self) -> usize {
1193        self.data.len() * 4
1194    }
1195}
1196
1197/// Stores the per-cell difference between two snapshots for compact undo.
1198#[derive(Debug, Clone)]
1199pub struct TerrainDiff {
1200    pub width: usize,
1201    pub height: usize,
1202    /// (cell_index, old_value, new_value)
1203    pub changes: Vec<(u32, f32, f32)>,
1204}
1205
1206impl TerrainDiff {
1207    pub fn compute(before: &TerrainSnapshot, after: &TerrainSnapshot) -> Self {
1208        assert_eq!(before.data.len(), after.data.len());
1209        let changes = before.data.iter().zip(after.data.iter()).enumerate()
1210            .filter_map(|(i, (&old, &new))| {
1211                if (old - new).abs() > 1e-7 { Some((i as u32, old, new)) } else { None }
1212            })
1213            .collect();
1214        Self { width: before.width, height: before.height, changes }
1215    }
1216
1217    pub fn apply(&self, hm: &mut heightmap::HeightMap) {
1218        for &(idx, _old, new) in &self.changes {
1219            hm.data[idx as usize] = new;
1220        }
1221    }
1222
1223    pub fn revert(&self, hm: &mut heightmap::HeightMap) {
1224        for &(idx, old, _new) in &self.changes {
1225            hm.data[idx as usize] = old;
1226        }
1227    }
1228
1229    pub fn changed_cell_count(&self) -> usize {
1230        self.changes.len()
1231    }
1232}
1233
1234/// Stack-based undo/redo manager for terrain edits.
1235pub struct TerrainUndoStack {
1236    undo: Vec<TerrainDiff>,
1237    redo: Vec<TerrainDiff>,
1238    pub max_depth: usize,
1239}
1240
1241impl TerrainUndoStack {
1242    pub fn new(max_depth: usize) -> Self {
1243        Self { undo: Vec::new(), redo: Vec::new(), max_depth }
1244    }
1245
1246    pub fn push(&mut self, diff: TerrainDiff) {
1247        if self.undo.len() >= self.max_depth {
1248            self.undo.remove(0);
1249        }
1250        self.undo.push(diff);
1251        self.redo.clear();
1252    }
1253
1254    pub fn undo(&mut self, hm: &mut heightmap::HeightMap) -> bool {
1255        if let Some(diff) = self.undo.pop() {
1256            diff.revert(hm);
1257            self.redo.push(diff);
1258            true
1259        } else { false }
1260    }
1261
1262    pub fn redo(&mut self, hm: &mut heightmap::HeightMap) -> bool {
1263        if let Some(diff) = self.redo.pop() {
1264            diff.apply(hm);
1265            self.undo.push(diff);
1266            true
1267        } else { false }
1268    }
1269
1270    pub fn can_undo(&self) -> bool { !self.undo.is_empty() }
1271    pub fn can_redo(&self) -> bool { !self.redo.is_empty() }
1272}
1273
1274// ─────────────────────────────────────────────────────────────────────────────
1275// Terrain statistics aggregator
1276// ─────────────────────────────────────────────────────────────────────────────
1277
1278/// Runtime statistics for a full terrain world.
1279#[derive(Debug, Default, Clone)]
1280pub struct TerrainWorldStats {
1281    pub total_chunks: u32,
1282    pub loaded_chunks: u32,
1283    pub visible_chunks: u32,
1284    pub total_triangles: u64,
1285    pub vegetation_instances: u64,
1286    pub memory_bytes: u64,
1287    pub last_update_ms: f64,
1288}
1289
1290impl TerrainWorldStats {
1291    pub fn memory_mb(&self) -> f64 {
1292        self.memory_bytes as f64 / (1024.0 * 1024.0)
1293    }
1294
1295    pub fn load_ratio(&self) -> f32 {
1296        if self.total_chunks == 0 { 0.0 } else {
1297            self.loaded_chunks as f32 / self.total_chunks as f32
1298        }
1299    }
1300
1301    pub fn describe(&self) -> String {
1302        format!(
1303            "Chunks: {}/{} loaded ({} visible), Tris: {}, Veg: {}, Mem: {:.1} MB",
1304            self.loaded_chunks, self.total_chunks, self.visible_chunks,
1305            self.total_triangles, self.vegetation_instances,
1306            self.memory_mb(),
1307        )
1308    }
1309}
1310
1311// ─────────────────────────────────────────────────────────────────────────────
1312// Terrain config presets
1313// ─────────────────────────────────────────────────────────────────────────────
1314
1315/// Named presets for common terrain configurations.
1316pub struct TerrainPresets;
1317
1318impl TerrainPresets {
1319    pub fn flat_plains() -> mod_types::TerrainConfig {
1320        mod_types::TerrainConfig {
1321            chunk_size: 64,
1322            lod_levels: 3,
1323            view_distance: 8,
1324            seed: 1,
1325        }
1326    }
1327
1328    pub fn mountainous() -> mod_types::TerrainConfig {
1329        mod_types::TerrainConfig {
1330            chunk_size: 128,
1331            lod_levels: 5,
1332            view_distance: 12,
1333            seed: 42,
1334        }
1335    }
1336
1337    pub fn ocean_archipelago() -> mod_types::TerrainConfig {
1338        mod_types::TerrainConfig {
1339            chunk_size: 128,
1340            lod_levels: 4,
1341            view_distance: 16,
1342            seed: 777,
1343        }
1344    }
1345
1346    pub fn desert_dunes() -> mod_types::TerrainConfig {
1347        mod_types::TerrainConfig {
1348            chunk_size: 64,
1349            lod_levels: 3,
1350            view_distance: 10,
1351            seed: 314,
1352        }
1353    }
1354}
1355
1356// ─────────────────────────────────────────────────────────────────────────────
1357// Additional tests for new types
1358// ─────────────────────────────────────────────────────────────────────────────
1359
1360#[cfg(test)]
1361mod extended_terrain_tests {
1362    use super::*;
1363
1364    #[test]
1365    fn test_terrain_event_queue_capacity() {
1366        let mut q = TerrainEventQueue::new(3);
1367        for i in 0..5 {
1368            q.push(TerrainEvent::WaterLevelChanged { old: i as f32, new: i as f32 + 1.0 });
1369        }
1370        // Should hold at most 3
1371        assert_eq!(q.len(), 3);
1372    }
1373
1374    #[test]
1375    fn test_terrain_event_queue_drain() {
1376        let mut q = TerrainEventQueue::new(10);
1377        q.push(TerrainEvent::WaterLevelChanged { old: 0.0, new: 1.0 });
1378        q.push(TerrainEvent::WaterLevelChanged { old: 1.0, new: 2.0 });
1379        let events = q.drain_all();
1380        assert_eq!(events.len(), 2);
1381        assert!(q.is_empty());
1382    }
1383
1384    #[test]
1385    fn test_terrain_snapshot_restore() {
1386        let mut hm = HeightMap::new(16, 16);
1387        for v in hm.data.iter_mut() { *v = 0.7; }
1388        let snap = TerrainSnapshot::capture(&hm, 1000);
1389        for v in hm.data.iter_mut() { *v = 0.2; }
1390        snap.restore_to(&mut hm);
1391        assert!((hm.data[0] - 0.7).abs() < 1e-5);
1392    }
1393
1394    #[test]
1395    fn test_terrain_diff_apply_revert() {
1396        let mut hm = HeightMap::new(16, 16);
1397        for v in hm.data.iter_mut() { *v = 0.5; }
1398        let before = TerrainSnapshot::capture(&hm, 0);
1399        hm.data[10] = 0.9;
1400        let after = TerrainSnapshot::capture(&hm, 1);
1401        let diff = TerrainDiff::compute(&before, &after);
1402        assert_eq!(diff.changed_cell_count(), 1);
1403        diff.revert(&mut hm);
1404        assert!((hm.data[10] - 0.5).abs() < 1e-5);
1405        diff.apply(&mut hm);
1406        assert!((hm.data[10] - 0.9).abs() < 1e-5);
1407    }
1408
1409    #[test]
1410    fn test_undo_stack() {
1411        let mut hm = HeightMap::new(16, 16);
1412        for v in hm.data.iter_mut() { *v = 0.5; }
1413        let before = TerrainSnapshot::capture(&hm, 0);
1414        hm.data[5] = 0.8;
1415        let after = TerrainSnapshot::capture(&hm, 1);
1416        let diff = TerrainDiff::compute(&before, &after);
1417        let mut stack = TerrainUndoStack::new(10);
1418        stack.push(diff);
1419        assert!(stack.can_undo());
1420        assert!(!stack.can_redo());
1421        assert!(stack.undo(&mut hm));
1422        assert!((hm.data[5] - 0.5).abs() < 1e-5);
1423        assert!(stack.can_redo());
1424        assert!(stack.redo(&mut hm));
1425        assert!((hm.data[5] - 0.8).abs() < 1e-5);
1426    }
1427
1428    #[test]
1429    fn test_terrain_world_stats() {
1430        let stats = TerrainWorldStats {
1431            total_chunks: 100,
1432            loaded_chunks: 40,
1433            visible_chunks: 25,
1434            total_triangles: 500_000,
1435            vegetation_instances: 12_000,
1436            memory_bytes: 64 * 1024 * 1024,
1437            last_update_ms: 16.7,
1438        };
1439        assert!((stats.memory_mb() - 64.0).abs() < 0.01);
1440        assert!((stats.load_ratio() - 0.4).abs() < 1e-4);
1441        let desc = stats.describe();
1442        assert!(desc.contains("40/100"));
1443    }
1444
1445    #[test]
1446    fn test_terrain_presets() {
1447        let plains = TerrainPresets::flat_plains();
1448        assert_eq!(plains.chunk_size, 64);
1449        let mountains = TerrainPresets::mountainous();
1450        assert!(mountains.lod_levels >= 5);
1451        let desert = TerrainPresets::desert_dunes();
1452        assert_eq!(desert.seed, 314);
1453    }
1454}