1use glam::Vec3;
7use crate::terrain::heightmap::HeightMap;
8use crate::terrain::biome::BiomeMap;
9use crate::terrain::vegetation::VegetationSystem;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
15pub struct ChunkCoord(pub i32, pub i32);
16
17impl ChunkCoord {
18 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 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 pub fn chebyshev_distance(self, other: ChunkCoord) -> i32 {
44 (self.0 - other.0).abs().max((self.1 - other.1).abs())
45 }
46
47 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 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 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum ChunkState {
96 Pending,
98 Generating,
100 Ready,
102 Evicting,
104 Serialized,
106}
107
108#[derive(Clone, Debug)]
112pub struct TerrainConfig {
113 pub chunk_size: usize,
115 pub view_distance: usize,
117 pub lod_levels: usize,
119 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
140pub 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 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 pub fn is_ready(&self) -> bool { self.state == ChunkState::Ready }
168
169 pub fn age_seconds(&self) -> f32 {
171 self.last_used.elapsed().as_secs_f32()
172 }
173
174 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#[derive(Clone, Debug)]
198pub struct ChunkGrid {
199 pub origin: ChunkCoord,
200 pub width: i32,
201 pub height: i32,
202}
203
204impl ChunkGrid {
205 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 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 pub fn count(&self) -> usize { (self.width * self.height) as usize }
227
228 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 pub fn at(&self, col: i32, row: i32) -> ChunkCoord {
238 ChunkCoord(self.origin.0 + col, self.origin.1 + row)
239 }
240
241 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#[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 pub fn center(&self) -> Vec3 { (self.min + self.max) * 0.5 }
276
277 pub fn half_extents(&self) -> Vec3 { (self.max - self.min) * 0.5 }
279
280 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 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 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#[derive(Clone, Debug)]
310pub struct ChunkHandle {
311 pub coord: ChunkCoord,
312 pub lod_level: u8,
313 pub state: ChunkState,
314 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#[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#[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 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 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#[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); }
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); 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}