Skip to main content

proof_engine/terrain/
mod_types.rs

1//! Shared terrain types used across submodules.
2//!
3//! This module defines the core data structures shared between the terrain
4//! submodules to avoid circular imports.
5
6use glam::Vec3;
7use crate::terrain::heightmap::HeightMap;
8use crate::terrain::biome::BiomeMap;
9use crate::terrain::vegetation::VegetationSystem;
10
11// ── ChunkCoord ────────────────────────────────────────────────────────────────
12
13/// Grid coordinate for a terrain chunk. (chunk_x, chunk_z) in chunk-space.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
15pub struct ChunkCoord(pub i32, pub i32);
16
17impl ChunkCoord {
18    /// Neighbor coordinates in 4 cardinal directions.
19    pub fn neighbors_4(self) -> [ChunkCoord; 4] {
20        [
21            ChunkCoord(self.0 - 1, self.1),
22            ChunkCoord(self.0 + 1, self.1),
23            ChunkCoord(self.0, self.1 - 1),
24            ChunkCoord(self.0, self.1 + 1),
25        ]
26    }
27
28    /// Neighbor coordinates in 8 directions (cardinal + diagonal).
29    pub fn neighbors_8(self) -> [ChunkCoord; 8] {
30        [
31            ChunkCoord(self.0 - 1, self.1 - 1),
32            ChunkCoord(self.0,     self.1 - 1),
33            ChunkCoord(self.0 + 1, self.1 - 1),
34            ChunkCoord(self.0 - 1, self.1),
35            ChunkCoord(self.0 + 1, self.1),
36            ChunkCoord(self.0 - 1, self.1 + 1),
37            ChunkCoord(self.0,     self.1 + 1),
38            ChunkCoord(self.0 + 1, self.1 + 1),
39        ]
40    }
41
42    /// Chebyshev distance to another chunk (max of abs differences).
43    pub fn chebyshev_distance(self, other: ChunkCoord) -> i32 {
44        (self.0 - other.0).abs().max((self.1 - other.1).abs())
45    }
46
47    /// Euclidean distance in chunk-space.
48    pub fn euclidean_distance(self, other: ChunkCoord) -> f32 {
49        let dx = (self.0 - other.0) as f32;
50        let dz = (self.1 - other.1) as f32;
51        (dx * dx + dz * dz).sqrt()
52    }
53
54    /// Convert chunk coord to world-space center position.
55    pub fn to_world_pos(self, chunk_size: f32) -> Vec3 {
56        Vec3::new(
57            (self.0 as f32 + 0.5) * chunk_size,
58            0.0,
59            (self.1 as f32 + 0.5) * chunk_size,
60        )
61    }
62
63    /// Distance from this chunk's center to an arbitrary world position.
64    pub fn distance_to_world_pos(self, world_pos: Vec3, chunk_size: f32) -> f32 {
65        let center = self.to_world_pos(chunk_size);
66        let dx = center.x - world_pos.x;
67        let dz = center.z - world_pos.z;
68        (dx * dx + dz * dz).sqrt()
69    }
70
71    /// Chunk coord from world position.
72    pub fn from_world_pos(world_pos: Vec3, chunk_size: f32) -> Self {
73        ChunkCoord(
74            (world_pos.x / chunk_size).floor() as i32,
75            (world_pos.z / chunk_size).floor() as i32,
76        )
77    }
78
79    /// True if this coord is within `radius` chunks of `other`.
80    pub fn within_radius(self, other: ChunkCoord, radius: i32) -> bool {
81        self.chebyshev_distance(other) <= radius
82    }
83}
84
85impl std::fmt::Display for ChunkCoord {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        write!(f, "({}, {})", self.0, self.1)
88    }
89}
90
91// ── ChunkState ────────────────────────────────────────────────────────────────
92
93/// Current lifecycle state of a terrain chunk.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum ChunkState {
96    /// Chunk is queued for generation.
97    Pending,
98    /// Chunk is currently being generated on a worker thread.
99    Generating,
100    /// Chunk data is ready for use.
101    Ready,
102    /// Chunk is staged for eviction from cache.
103    Evicting,
104    /// Chunk has been serialized to disk.
105    Serialized,
106}
107
108// ── TerrainConfig ─────────────────────────────────────────────────────────────
109
110/// Top-level configuration for the terrain system.
111#[derive(Clone, Debug)]
112pub struct TerrainConfig {
113    /// Size of each chunk in terrain cells (e.g. 64, 128, 256).
114    pub chunk_size:    usize,
115    /// Number of chunks visible in each direction from the camera.
116    pub view_distance: usize,
117    /// Number of LOD levels (1 = no LOD, 4 = aggressive).
118    pub lod_levels:    usize,
119    /// World generation seed.
120    pub seed:          u64,
121}
122
123impl Default for TerrainConfig {
124    fn default() -> Self {
125        Self {
126            chunk_size:    64,
127            view_distance: 8,
128            lod_levels:    4,
129            seed:          12345,
130        }
131    }
132}
133
134impl TerrainConfig {
135    pub fn new(chunk_size: usize, view_distance: usize, lod_levels: usize, seed: u64) -> Self {
136        Self { chunk_size, view_distance, lod_levels, seed }
137    }
138}
139
140// ── TerrainChunk ──────────────────────────────────────────────────────────────
141
142/// A single terrain chunk: one tile of the infinite world grid.
143pub struct TerrainChunk {
144    pub coord:       ChunkCoord,
145    pub heightmap:   HeightMap,
146    pub biome_map:   Option<BiomeMap>,
147    pub vegetation:  Option<VegetationSystem>,
148    pub lod_level:   u8,
149    pub state:       ChunkState,
150    pub last_used:   std::time::Instant,
151    pub seed:        u64,
152}
153
154impl TerrainChunk {
155    /// World-space bounds: (min, max) corner of this chunk.
156    pub fn world_bounds(&self, chunk_size: f32) -> (Vec3, Vec3) {
157        let min_x = self.coord.0 as f32 * chunk_size;
158        let min_z = self.coord.1 as f32 * chunk_size;
159        let max_x = min_x + chunk_size;
160        let max_z = min_z + chunk_size;
161        let min_h = self.heightmap.min_value() * 100.0;
162        let max_h = self.heightmap.max_value() * 100.0;
163        (Vec3::new(min_x, min_h, min_z), Vec3::new(max_x, max_h, max_z))
164    }
165
166    /// Is the chunk fully ready for rendering?
167    pub fn is_ready(&self) -> bool { self.state == ChunkState::Ready }
168
169    /// Seconds since this chunk was last accessed.
170    pub fn age_seconds(&self) -> f32 {
171        self.last_used.elapsed().as_secs_f32()
172    }
173
174    /// Approximate memory usage in bytes.
175    pub fn memory_bytes(&self) -> usize {
176        let hm = self.heightmap.data.len() * 4;
177        let bm = self.biome_map.as_ref().map(|b| b.biomes.len()).unwrap_or(0) * 1;
178        let veg = self.vegetation.as_ref().map(|v| v.instances.len() * 64).unwrap_or(0);
179        std::mem::size_of::<TerrainChunk>() + hm + bm + veg
180    }
181}
182
183impl std::fmt::Debug for TerrainChunk {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        f.debug_struct("TerrainChunk")
186            .field("coord", &self.coord)
187            .field("lod_level", &self.lod_level)
188            .field("state", &self.state)
189            .field("heightmap_size", &(self.heightmap.width, self.heightmap.height))
190            .finish()
191    }
192}
193
194// ── ChunkGrid ─────────────────────────────────────────────────────────────────
195
196/// A 2D grid of chunk coordinates within a rectangular region.
197#[derive(Clone, Debug)]
198pub struct ChunkGrid {
199    pub origin: ChunkCoord,
200    pub width:  i32,
201    pub height: i32,
202}
203
204impl ChunkGrid {
205    /// Create a grid of chunks centred on `center` with given half-extent.
206    pub fn around(center: ChunkCoord, half_extent: i32) -> Self {
207        Self {
208            origin: ChunkCoord(center.0 - half_extent, center.1 - half_extent),
209            width:  half_extent * 2 + 1,
210            height: half_extent * 2 + 1,
211        }
212    }
213
214    /// Iterate all coords in the grid.
215    pub fn iter(&self) -> impl Iterator<Item = ChunkCoord> + '_ {
216        let ox = self.origin.0;
217        let oy = self.origin.1;
218        let w  = self.width;
219        let h  = self.height;
220        (0..h).flat_map(move |dy| {
221            (0..w).map(move |dx| ChunkCoord(ox + dx, oy + dy))
222        })
223    }
224
225    /// Total number of chunks in this grid.
226    pub fn count(&self) -> usize { (self.width * self.height) as usize }
227
228    /// Test if a coord is within this grid.
229    pub fn contains(&self, c: ChunkCoord) -> bool {
230        c.0 >= self.origin.0
231            && c.0 < self.origin.0 + self.width
232            && c.1 >= self.origin.1
233            && c.1 < self.origin.1 + self.height
234    }
235
236    /// Convert a grid-relative (col, row) to ChunkCoord.
237    pub fn at(&self, col: i32, row: i32) -> ChunkCoord {
238        ChunkCoord(self.origin.0 + col, self.origin.1 + row)
239    }
240
241    /// Coords sorted by distance to center.
242    pub fn sorted_by_distance(&self, center: ChunkCoord) -> Vec<ChunkCoord> {
243        let mut coords: Vec<ChunkCoord> = self.iter().collect();
244        coords.sort_by(|a, b| {
245            let da = a.euclidean_distance(center);
246            let db = b.euclidean_distance(center);
247            da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
248        });
249        coords
250    }
251}
252
253// ── ChunkBounds ───────────────────────────────────────────────────────────────
254
255/// World-space AABB of a chunk.
256#[derive(Clone, Copy, Debug)]
257pub struct ChunkBounds {
258    pub min: Vec3,
259    pub max: Vec3,
260}
261
262impl ChunkBounds {
263    pub fn new(min: Vec3, max: Vec3) -> Self { Self { min, max } }
264
265    pub fn from_chunk(coord: ChunkCoord, chunk_size: f32, height_scale: f32) -> Self {
266        let x0 = coord.0 as f32 * chunk_size;
267        let z0 = coord.1 as f32 * chunk_size;
268        Self {
269            min: Vec3::new(x0, 0.0, z0),
270            max: Vec3::new(x0 + chunk_size, height_scale, z0 + chunk_size),
271        }
272    }
273
274    /// Center of the bounding box.
275    pub fn center(&self) -> Vec3 { (self.min + self.max) * 0.5 }
276
277    /// Half-extents of the bounding box.
278    pub fn half_extents(&self) -> Vec3 { (self.max - self.min) * 0.5 }
279
280    /// True if this box intersects another.
281    pub fn intersects(&self, other: &ChunkBounds) -> bool {
282        self.min.x <= other.max.x && self.max.x >= other.min.x
283            && self.min.y <= other.max.y && self.max.y >= other.min.y
284            && self.min.z <= other.max.z && self.max.z >= other.min.z
285    }
286
287    /// True if a point is inside this box.
288    pub fn contains_point(&self, p: Vec3) -> bool {
289        p.x >= self.min.x && p.x <= self.max.x
290            && p.y >= self.min.y && p.y <= self.max.y
291            && p.z >= self.min.z && p.z <= self.max.z
292    }
293
294    /// Signed distance from a point to this box (negative = inside).
295    pub fn sdf(&self, p: Vec3) -> f32 {
296        let q = Vec3::new(
297            (p.x - self.center().x).abs() - self.half_extents().x,
298            (p.y - self.center().y).abs() - self.half_extents().y,
299            (p.z - self.center().z).abs() - self.half_extents().z,
300        );
301        let max_q = Vec3::new(q.x.max(0.0), q.y.max(0.0), q.z.max(0.0));
302        max_q.length() + q.x.max(q.y).max(q.z).min(0.0)
303    }
304}
305
306// ── ChunkHandle ───────────────────────────────────────────────────────────────
307
308/// A lightweight reference-counted handle to a chunk (for use outside the cache).
309#[derive(Clone, Debug)]
310pub struct ChunkHandle {
311    pub coord:     ChunkCoord,
312    pub lod_level: u8,
313    pub state:     ChunkState,
314    /// Version counter for detecting stale handles.
315    pub version:   u32,
316}
317
318impl ChunkHandle {
319    pub fn new(coord: ChunkCoord, lod_level: u8) -> Self {
320        Self { coord, lod_level, state: ChunkState::Pending, version: 0 }
321    }
322
323    pub fn is_ready(&self) -> bool { self.state == ChunkState::Ready }
324
325    pub fn advance_version(&mut self) { self.version = self.version.wrapping_add(1); }
326}
327
328// ── TerrainRegion ─────────────────────────────────────────────────────────────
329
330/// Describes a named rectangular region of the world.
331#[derive(Clone, Debug)]
332pub struct TerrainRegion {
333    pub name:   String,
334    pub min:    ChunkCoord,
335    pub max:    ChunkCoord,
336    pub biome_hint: crate::terrain::biome::BiomeType,
337}
338
339impl TerrainRegion {
340    pub fn new(name: &str, min: ChunkCoord, max: ChunkCoord) -> Self {
341        Self {
342            name: name.to_string(),
343            min, max,
344            biome_hint: crate::terrain::biome::BiomeType::Grassland,
345        }
346    }
347
348    pub fn contains(&self, coord: ChunkCoord) -> bool {
349        coord.0 >= self.min.0 && coord.0 <= self.max.0
350            && coord.1 >= self.min.1 && coord.1 <= self.max.1
351    }
352
353    pub fn area(&self) -> usize {
354        let w = (self.max.0 - self.min.0 + 1).max(0) as usize;
355        let h = (self.max.1 - self.min.1 + 1).max(0) as usize;
356        w * h
357    }
358
359    pub fn center(&self) -> ChunkCoord {
360        ChunkCoord(
361            (self.min.0 + self.max.0) / 2,
362            (self.min.1 + self.max.1) / 2,
363        )
364    }
365}
366
367// ── WorldSeed ─────────────────────────────────────────────────────────────────
368
369/// A structured seed for reproducible world generation.
370#[derive(Clone, Debug)]
371pub struct WorldSeed {
372    pub base_seed:   u64,
373    pub terrain_seed: u64,
374    pub biome_seed:  u64,
375    pub vegetation_seed: u64,
376    pub weather_seed: u64,
377    pub name:        String,
378}
379
380impl WorldSeed {
381    pub fn from_u64(seed: u64) -> Self {
382        Self {
383            base_seed:       seed,
384            terrain_seed:    seed.wrapping_mul(0x9e3779b97f4a7c15),
385            biome_seed:      seed.wrapping_mul(0x6c62272e07bb0142),
386            vegetation_seed: seed.wrapping_mul(0xbf58476d1ce4e5b9),
387            weather_seed:    seed.wrapping_mul(0x94d049bb133111eb),
388            name:            format!("World-{:016X}", seed),
389        }
390    }
391
392    pub fn named(mut self, name: &str) -> Self {
393        self.name = name.to_string();
394        self
395    }
396
397    /// Derive a per-chunk seed.
398    pub fn chunk_seed(&self, coord: ChunkCoord) -> u64 {
399        let cx = coord.0 as u64;
400        let cz = coord.1 as u64;
401        self.terrain_seed
402            .wrapping_add(cx.wrapping_mul(0x9e3779b97f4a7c15))
403            .wrapping_add(cz.wrapping_mul(0x6c62272e07bb0142))
404    }
405
406    /// Derive a per-biome seed.
407    pub fn biome_chunk_seed(&self, coord: ChunkCoord) -> u64 {
408        let cx = coord.0 as u64;
409        let cz = coord.1 as u64;
410        self.biome_seed
411            .wrapping_add(cx.wrapping_mul(0xbf58476d1ce4e5b9))
412            .wrapping_add(cz.wrapping_mul(0x94d049bb133111eb))
413    }
414}
415
416// ── Tests ─────────────────────────────────────────────────────────────────────
417
418#[cfg(test)]
419mod mod_types_tests {
420    use super::*;
421    use glam::Vec3;
422
423    #[test]
424    fn test_chunk_grid_count() {
425        let grid = ChunkGrid::around(ChunkCoord(0, 0), 2);
426        assert_eq!(grid.count(), 25); // 5x5
427    }
428
429    #[test]
430    fn test_chunk_grid_iter() {
431        let grid = ChunkGrid::around(ChunkCoord(0, 0), 1);
432        let coords: Vec<ChunkCoord> = grid.iter().collect();
433        assert_eq!(coords.len(), 9); // 3x3
434        assert!(coords.contains(&ChunkCoord(0, 0)));
435        assert!(coords.contains(&ChunkCoord(-1, -1)));
436        assert!(coords.contains(&ChunkCoord(1, 1)));
437    }
438
439    #[test]
440    fn test_chunk_grid_contains() {
441        let grid = ChunkGrid::around(ChunkCoord(5, 5), 2);
442        assert!(grid.contains(ChunkCoord(5, 5)));
443        assert!(grid.contains(ChunkCoord(3, 3)));
444        assert!(!grid.contains(ChunkCoord(0, 0)));
445    }
446
447    #[test]
448    fn test_chunk_grid_sorted_by_distance() {
449        let grid = ChunkGrid::around(ChunkCoord(0, 0), 2);
450        let sorted = grid.sorted_by_distance(ChunkCoord(0, 0));
451        assert_eq!(sorted[0], ChunkCoord(0, 0));
452    }
453
454    #[test]
455    fn test_chunk_bounds_center() {
456        let b = ChunkBounds::from_chunk(ChunkCoord(0, 0), 64.0, 100.0);
457        let c = b.center();
458        assert!((c.x - 32.0).abs() < 1e-4);
459        assert!((c.z - 32.0).abs() < 1e-4);
460    }
461
462    #[test]
463    fn test_chunk_bounds_intersects() {
464        let b1 = ChunkBounds::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(64.0, 100.0, 64.0));
465        let b2 = ChunkBounds::new(Vec3::new(32.0, 0.0, 32.0), Vec3::new(96.0, 100.0, 96.0));
466        let b3 = ChunkBounds::new(Vec3::new(200.0, 0.0, 200.0), Vec3::new(264.0, 100.0, 264.0));
467        assert!(b1.intersects(&b2));
468        assert!(!b1.intersects(&b3));
469    }
470
471    #[test]
472    fn test_chunk_bounds_contains_point() {
473        let b = ChunkBounds::from_chunk(ChunkCoord(0, 0), 64.0, 100.0);
474        assert!(b.contains_point(Vec3::new(32.0, 50.0, 32.0)));
475        assert!(!b.contains_point(Vec3::new(100.0, 50.0, 32.0)));
476    }
477
478    #[test]
479    fn test_chunk_handle() {
480        let mut h = ChunkHandle::new(ChunkCoord(3, 4), 0);
481        assert!(!h.is_ready());
482        h.state = ChunkState::Ready;
483        assert!(h.is_ready());
484        h.advance_version();
485        assert_eq!(h.version, 1);
486    }
487
488    #[test]
489    fn test_terrain_region() {
490        let r = TerrainRegion::new("Forest", ChunkCoord(0, 0), ChunkCoord(9, 9));
491        assert_eq!(r.area(), 100);
492        assert!(r.contains(ChunkCoord(5, 5)));
493        assert!(!r.contains(ChunkCoord(10, 10)));
494        assert_eq!(r.center(), ChunkCoord(4, 4));
495    }
496
497    #[test]
498    fn test_world_seed() {
499        let ws = WorldSeed::from_u64(12345).named("TestWorld");
500        assert_eq!(ws.name, "TestWorld");
501        assert_ne!(ws.terrain_seed, ws.biome_seed);
502        let s1 = ws.chunk_seed(ChunkCoord(0, 0));
503        let s2 = ws.chunk_seed(ChunkCoord(1, 0));
504        assert_ne!(s1, s2);
505    }
506
507    #[test]
508    fn test_terrain_config_default() {
509        let c = TerrainConfig::default();
510        assert_eq!(c.chunk_size, 64);
511        assert_eq!(c.view_distance, 8);
512        assert_eq!(c.lod_levels, 4);
513    }
514
515    #[test]
516    fn test_chunk_state_variants() {
517        let states = [
518            ChunkState::Pending, ChunkState::Generating, ChunkState::Ready,
519            ChunkState::Evicting, ChunkState::Serialized,
520        ];
521        for s in states { let _ = s; }
522    }
523}