1use super::heightmap::HeightMap;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
13pub enum BiomeKind {
14 Ocean,
16 ShallowWater,
17 Lake,
18 River,
19 Desert,
21 HotDesert,
22 SemiArid,
23 TropicalRainforest,
25 TropicalDryForest,
26 Savanna,
27 Shrubland,
29 TemperateRainforest,
30 TemperateSeasonalForest,
31 TemperateGrassland,
32 BorealForest, Tundra,
35 Alpine,
36 Glacier,
38 Beach,
39 Wetland,
40 Mangrove,
41 Volcano,
42}
43
44impl BiomeKind {
45 pub fn name(self) -> &'static str {
47 match self {
48 BiomeKind::Ocean => "Ocean",
49 BiomeKind::ShallowWater => "Shallow Water",
50 BiomeKind::Lake => "Lake",
51 BiomeKind::River => "River",
52 BiomeKind::Desert => "Desert",
53 BiomeKind::HotDesert => "Hot Desert",
54 BiomeKind::SemiArid => "Semi-Arid",
55 BiomeKind::TropicalRainforest => "Tropical Rainforest",
56 BiomeKind::TropicalDryForest => "Tropical Dry Forest",
57 BiomeKind::Savanna => "Savanna",
58 BiomeKind::Shrubland => "Shrubland",
59 BiomeKind::TemperateRainforest => "Temperate Rainforest",
60 BiomeKind::TemperateSeasonalForest => "Temperate Seasonal Forest",
61 BiomeKind::TemperateGrassland => "Temperate Grassland",
62 BiomeKind::BorealForest => "Boreal Forest (Taiga)",
63 BiomeKind::Tundra => "Tundra",
64 BiomeKind::Alpine => "Alpine",
65 BiomeKind::Glacier => "Glacier",
66 BiomeKind::Beach => "Beach",
67 BiomeKind::Wetland => "Wetland",
68 BiomeKind::Mangrove => "Mangrove",
69 BiomeKind::Volcano => "Volcano",
70 }
71 }
72
73 pub fn map_color(self) -> [u8; 3] {
75 match self {
76 BiomeKind::Ocean => [0, 60, 120],
77 BiomeKind::ShallowWater => [30, 120, 180],
78 BiomeKind::Lake => [50, 100, 200],
79 BiomeKind::River => [50, 130, 220],
80 BiomeKind::Desert => [230, 200, 130],
81 BiomeKind::HotDesert => [240, 190, 100],
82 BiomeKind::SemiArid => [210, 185, 130],
83 BiomeKind::TropicalRainforest => [10, 100, 20],
84 BiomeKind::TropicalDryForest => [50, 130, 40],
85 BiomeKind::Savanna => [160, 180, 60],
86 BiomeKind::Shrubland => [130, 150, 80],
87 BiomeKind::TemperateRainforest => [30, 100, 60],
88 BiomeKind::TemperateSeasonalForest => [60, 130, 50],
89 BiomeKind::TemperateGrassland => [130, 170, 80],
90 BiomeKind::BorealForest => [50, 100, 70],
91 BiomeKind::Tundra => [160, 170, 140],
92 BiomeKind::Alpine => [200, 200, 200],
93 BiomeKind::Glacier => [230, 245, 255],
94 BiomeKind::Beach => [230, 210, 150],
95 BiomeKind::Wetland => [60, 130, 100],
96 BiomeKind::Mangrove => [30, 90, 60],
97 BiomeKind::Volcano => [90, 30, 20],
98 }
99 }
100
101 pub fn is_water(self) -> bool {
103 matches!(self,
104 BiomeKind::Ocean | BiomeKind::ShallowWater |
105 BiomeKind::Lake | BiomeKind::River)
106 }
107
108 pub fn is_forested(self) -> bool {
110 matches!(self,
111 BiomeKind::TropicalRainforest |
112 BiomeKind::TropicalDryForest |
113 BiomeKind::TemperateRainforest |
114 BiomeKind::TemperateSeasonalForest |
115 BiomeKind::BorealForest |
116 BiomeKind::Mangrove)
117 }
118}
119
120#[derive(Clone, Debug)]
124pub struct ClimateCell {
125 pub temperature: f32,
127 pub moisture: f32,
129 pub altitude: f32,
131 pub continentality: f32,
133}
134
135pub struct WhittakerClassifier;
142
143impl WhittakerClassifier {
144 pub fn classify(cell: &ClimateCell) -> BiomeKind {
146 let t = cell.temperature;
147 let m = cell.moisture;
148 let a = cell.altitude;
149
150 if a > 0.92 { return BiomeKind::Glacier; }
152 if a > 0.80 { return BiomeKind::Alpine; }
153
154 if t < 0.10 {
157 return if a > 0.70 { BiomeKind::Alpine } else { BiomeKind::Glacier };
158 }
159 if t < 0.20 {
160 return BiomeKind::Tundra;
161 }
162
163 if t < 0.35 {
165 return if m > 0.40 {
166 BiomeKind::BorealForest
167 } else {
168 BiomeKind::Tundra
169 };
170 }
171
172 if t < 0.50 {
174 return if m > 0.70 {
175 BiomeKind::TemperateRainforest
176 } else if m > 0.40 {
177 BiomeKind::TemperateSeasonalForest
178 } else if m > 0.20 {
179 BiomeKind::TemperateGrassland
180 } else {
181 BiomeKind::Desert
182 };
183 }
184
185 if t < 0.65 {
187 return if m > 0.65 {
188 BiomeKind::TemperateRainforest
189 } else if m > 0.40 {
190 BiomeKind::TemperateSeasonalForest
191 } else if m > 0.25 {
192 BiomeKind::Shrubland
193 } else if m > 0.12 {
194 BiomeKind::TemperateGrassland
195 } else {
196 BiomeKind::SemiArid
197 };
198 }
199
200 if t < 0.80 {
202 return if m > 0.70 {
203 BiomeKind::TropicalDryForest
204 } else if m > 0.40 {
205 BiomeKind::Savanna
206 } else if m > 0.20 {
207 BiomeKind::SemiArid
208 } else {
209 BiomeKind::HotDesert
210 };
211 }
212
213 if m > 0.75 {
215 BiomeKind::TropicalRainforest
216 } else if m > 0.50 {
217 BiomeKind::TropicalDryForest
218 } else if m > 0.30 {
219 BiomeKind::Savanna
220 } else if m > 0.15 {
221 BiomeKind::SemiArid
222 } else {
223 BiomeKind::HotDesert
224 }
225 }
226}
227
228#[derive(Clone, Debug)]
232pub struct BiomeMap {
233 pub width: usize,
234 pub height: usize,
235 pub biomes: Vec<BiomeKind>,
237 pub blend: Vec<BiomeBlend>,
239 pub climate: Vec<ClimateCell>,
241}
242
243#[derive(Clone, Debug, Default)]
245pub struct BiomeBlend {
246 pub entries: [(BiomeKind, f32); 4],
248 pub count: usize,
249}
250
251impl BiomeBlend {
252 pub fn new_single(biome: BiomeKind) -> Self {
253 let mut b = Self::default();
254 b.entries[0] = (biome, 1.0);
255 b.count = 1;
256 b
257 }
258}
259
260impl Default for BiomeKind {
261 fn default() -> Self { BiomeKind::TemperateGrassland }
262}
263
264impl BiomeMap {
265 #[inline]
266 pub fn idx(&self, x: usize, y: usize) -> usize { y * self.width + x }
267
268 pub fn get(&self, x: usize, y: usize) -> BiomeKind { self.biomes[y * self.width + x] }
269
270 pub fn set(&mut self, x: usize, y: usize, b: BiomeKind) {
271 self.biomes[y * self.width + x] = b;
272 }
273}
274
275pub struct ClimateGenerator {
283 pub equatorial_band: f32, pub moisture_falloff: f32, pub seed: u64,
286}
287
288impl ClimateGenerator {
289 pub fn new(seed: u64) -> Self {
290 Self {
291 equatorial_band: 0.5,
292 moisture_falloff: 0.6,
293 seed,
294 }
295 }
296
297 pub fn generate(&self, heightmap: &HeightMap, sea_level: f32) -> Vec<ClimateCell> {
299 let w = heightmap.width;
300 let h = heightmap.height;
301 let mut cells = Vec::with_capacity(w * h);
302
303 let ocean_dist = self.ocean_distance_map(heightmap, sea_level);
305
306 let noise = MoistureNoise::new(self.seed);
308
309 for y in 0..h {
310 for x in 0..w {
311 let altitude = heightmap.get(x, y);
312 let lat = (y as f32 / h as f32 - self.equatorial_band).abs() * 2.0;
314 let lat = lat.clamp(0.0, 1.0);
315
316 let base_temp = 1.0 - lat;
318 let lapse = altitude * 0.65;
320 let temperature = (base_temp - lapse).clamp(0.0, 1.0);
321
322 let max_dist = (w.max(h) / 2) as f32;
324 let continentality = (ocean_dist[y * w + x] as f32 / max_dist).clamp(0.0, 1.0);
325
326 let itcz = 1.0 - (lat * 2.0 - 0.3).abs().min(1.0);
329 let base_moist = (1.0 - continentality * self.moisture_falloff + itcz * 0.3).clamp(0.0, 1.0);
330 let nx = x as f32 / w as f32 * 3.7;
332 let ny = y as f32 / h as f32 * 3.7;
333 let moist_noise = noise.sample(nx, ny) * 0.25;
334 let moisture = (base_moist + moist_noise - altitude * 0.15).clamp(0.0, 1.0);
335
336 cells.push(ClimateCell { temperature, moisture, altitude, continentality });
337 }
338 }
339
340 cells
341 }
342
343 fn ocean_distance_map(&self, heightmap: &HeightMap, sea_level: f32) -> Vec<u32> {
345 use std::collections::VecDeque;
346 let w = heightmap.width;
347 let h = heightmap.height;
348 let mut dist = vec![u32::MAX; w * h];
349 let mut queue = VecDeque::new();
350
351 for y in 0..h {
353 for x in 0..w {
354 if heightmap.get(x, y) < sea_level {
355 dist[y * w + x] = 0;
356 queue.push_back((x, y));
357 }
358 }
359 }
360
361 while let Some((x, y)) = queue.pop_front() {
363 let d = dist[y * w + x];
364 for (dx, dy) in &[(-1i64, 0i64), (1, 0), (0, -1), (0, 1)] {
365 let nx = x as i64 + dx;
366 let ny = y as i64 + dy;
367 if nx < 0 || ny < 0 || nx >= w as i64 || ny >= h as i64 { continue; }
368 let idx = ny as usize * w + nx as usize;
369 if dist[idx] == u32::MAX {
370 dist[idx] = d + 1;
371 queue.push_back((nx as usize, ny as usize));
372 }
373 }
374 }
375
376 dist
377 }
378}
379
380struct MoistureNoise {
382 seed: u64,
383}
384
385impl MoistureNoise {
386 fn new(seed: u64) -> Self { Self { seed } }
387
388 fn sample(&self, x: f32, y: f32) -> f32 {
389 let xi = x.floor() as i64;
390 let yi = y.floor() as i64;
391 let xf = x - xi as f32;
392 let yf = y - yi as f32;
393 let ux = xf * xf * (3.0 - 2.0 * xf);
394 let uy = yf * yf * (3.0 - 2.0 * yf);
395 let r = |ix: i64, iy: i64| -> f32 {
396 let mut h = self.seed;
397 h ^= ix.unsigned_abs().wrapping_mul(374761393);
398 h ^= iy.unsigned_abs().wrapping_mul(668265263);
399 h = h.wrapping_mul(0x9e3779b97f4a7c15);
400 h ^= h >> 33;
401 if ix < 0 { h ^= 0xabcd; }
402 if iy < 0 { h ^= 0xef01; }
403 (h >> 11) as f32 / (1u64 << 53) as f32
404 };
405 let v = r(xi, yi) * (1.0-ux)*(1.0-uy)
406 + r(xi+1,yi) * ux *(1.0-uy)
407 + r(xi,yi+1) * (1.0-ux)* uy
408 + r(xi+1,yi+1)* ux * uy;
409 v
410 }
411}
412
413pub struct BiomePlacer {
417 pub sea_level: f32,
418 pub shallow_water_depth: f32,
419 pub beach_width: f32,
420 pub glacier_altitude: f32,
421 pub enable_rivers: bool,
422 pub enable_lakes: bool,
423 pub enable_mangroves: bool,
424 pub river_count: usize,
425 pub seed: u64,
426}
427
428impl BiomePlacer {
429 pub fn new(seed: u64) -> Self {
430 Self {
431 sea_level: 0.35,
432 shallow_water_depth: 0.08,
433 beach_width: 0.02,
434 glacier_altitude: 0.92,
435 enable_rivers: true,
436 enable_lakes: true,
437 enable_mangroves: true,
438 river_count: 8,
439 seed,
440 }
441 }
442
443 pub fn generate(&self, heightmap: &HeightMap) -> BiomeMap {
445 let w = heightmap.width;
446 let h = heightmap.height;
447
448 let climate_gen = ClimateGenerator::new(self.seed);
450 let climate = climate_gen.generate(heightmap, self.sea_level);
451
452 let mut biomes = Vec::with_capacity(w * h);
454 for y in 0..h {
455 for x in 0..w {
456 let alt = heightmap.get(x, y);
457 let cell = &climate[y * w + x];
458 let biome = if alt < self.sea_level - self.shallow_water_depth {
459 BiomeKind::Ocean
460 } else if alt < self.sea_level {
461 BiomeKind::ShallowWater
462 } else if alt < self.sea_level + self.beach_width {
463 BiomeKind::Beach
464 } else {
465 WhittakerClassifier::classify(cell)
466 };
467 biomes.push(biome);
468 }
469 }
470
471 let mut map = BiomeMap {
472 width: w,
473 height: h,
474 biomes,
475 blend: vec![BiomeBlend::new_single(BiomeKind::TemperateGrassland); w * h],
476 climate,
477 };
478
479 if self.enable_rivers {
481 self.carve_rivers(&mut map, heightmap);
482 }
483 if self.enable_lakes {
484 self.fill_lakes(&mut map, heightmap);
485 }
486 if self.enable_mangroves {
487 self.place_mangroves(&mut map, heightmap);
488 }
489
490 self.compute_biome_blending(&mut map);
492
493 map
494 }
495
496 fn carve_rivers(&self, map: &mut BiomeMap, heightmap: &HeightMap) {
499 let w = heightmap.width;
500 let h = heightmap.height;
501 let mut rng = crate::terrain::heightmap::Rng::new(self.seed ^ 0xbeef);
502
503 for _ in 0..self.river_count {
504 let mut tries = 0;
506 let (mut x, mut y) = loop {
507 let cx = rng.next_i32_range(2, w as i32 - 2) as usize;
508 let cy = rng.next_i32_range(2, h as i32 - 2) as usize;
509 let alt = heightmap.get(cx, cy);
510 if alt > 0.65 && map.get(cx, cy) != BiomeKind::Ocean {
511 break (cx, cy);
512 }
513 tries += 1;
514 if tries > 200 { break (cx, cy); }
515 };
516
517 let max_steps = (w + h) * 2;
519 for _ in 0..max_steps {
520 map.set(x, y, BiomeKind::River);
521
522 let mut best_h = heightmap.get(x, y);
524 let mut best = None;
525 for (dx, dy) in &[(-1i64, 0i64), (1, 0), (0, -1), (0, 1),
526 (-1, -1), (1, -1), (-1, 1), (1, 1)] {
527 let nx = x as i64 + dx;
528 let ny = y as i64 + dy;
529 if nx < 0 || ny < 0 || nx >= w as i64 || ny >= h as i64 { continue; }
530 let nh = heightmap.get(nx as usize, ny as usize);
531 if nh < best_h {
532 best_h = nh;
533 best = Some((nx as usize, ny as usize));
534 }
535 }
536
537 match best {
538 Some((nx, ny)) => {
539 if map.get(nx, ny) == BiomeKind::Ocean
541 || map.get(nx, ny) == BiomeKind::ShallowWater {
542 break;
543 }
544 x = nx;
545 y = ny;
546 }
547 None => break, }
549 }
550 }
551 }
552
553 fn fill_lakes(&self, map: &mut BiomeMap, heightmap: &HeightMap) {
556 let w = heightmap.width;
557 let h = heightmap.height;
558
559 let mut visited = vec![false; w * h];
562
563 for y in 1..h.saturating_sub(1) {
564 for x in 1..w.saturating_sub(1) {
565 if visited[y * w + x] { continue; }
566 let biome = map.get(x, y);
567 if biome.is_water() { continue; }
568
569 let center_h = heightmap.get(x, y);
570 let is_depression = [(0i64, -1i64), (0, 1), (-1, 0), (1, 0)]
571 .iter()
572 .all(|(dx, dy)| {
573 let nx = (x as i64 + dx) as usize;
574 let ny = (y as i64 + dy) as usize;
575 heightmap.get(nx, ny) > center_h
576 });
577
578 if is_depression && heightmap.get(x, y) < self.sea_level + 0.15 {
579 self.flood_fill_lake(map, heightmap, x, y, &mut visited);
581 }
582 }
583 }
584 }
585
586 fn flood_fill_lake(
587 &self,
588 map: &mut BiomeMap,
589 heightmap: &HeightMap,
590 sx: usize,
591 sy: usize,
592 visited: &mut Vec<bool>,
593 ) {
594 use std::collections::VecDeque;
595 let w = heightmap.width;
596 let h = heightmap.height;
597 let seed_h = heightmap.get(sx, sy);
598 let lake_level = seed_h + 0.03; let mut queue = VecDeque::new();
601 queue.push_back((sx, sy));
602 visited[sy * w + sx] = true;
603
604 let mut cells_filled = 0usize;
605 let max_lake_cells = 500;
606
607 while let Some((x, y)) = queue.pop_front() {
608 if cells_filled >= max_lake_cells { break; }
609 if !map.get(x, y).is_water() {
610 map.set(x, y, BiomeKind::Lake);
611 cells_filled += 1;
612 }
613
614 for (dx, dy) in &[(-1i64, 0i64), (1, 0), (0, -1), (0, 1)] {
615 let nx = x as i64 + dx;
616 let ny = y as i64 + dy;
617 if nx < 0 || ny < 0 || nx >= w as i64 || ny >= h as i64 { continue; }
618 let ni = ny as usize * w + nx as usize;
619 if visited[ni] { continue; }
620 let nh = heightmap.get(nx as usize, ny as usize);
621 if nh <= lake_level && !map.get(nx as usize, ny as usize).is_water() {
622 visited[ni] = true;
623 queue.push_back((nx as usize, ny as usize));
624 }
625 }
626 }
627 }
628
629 fn place_mangroves(&self, map: &mut BiomeMap, heightmap: &HeightMap) {
632 let w = heightmap.width;
633 let h = heightmap.height;
634
635 for y in 1..h.saturating_sub(1) {
636 for x in 1..w.saturating_sub(1) {
637 let biome = map.get(x, y);
638 if biome != BiomeKind::Beach { continue; }
639 let climate = &map.climate[y * w + x];
640 if climate.temperature < 0.65 { continue; }
642 let adj_water = [(-1i64, 0i64), (1, 0), (0, -1), (0, 1)]
644 .iter()
645 .any(|(dx, dy)| {
646 let nx = (x as i64 + dx) as usize;
647 let ny = (y as i64 + dy) as usize;
648 matches!(map.get(nx, ny), BiomeKind::Ocean | BiomeKind::ShallowWater)
649 });
650 if adj_water {
651 map.set(x, y, BiomeKind::Mangrove);
652 }
653 }
654 }
655 }
656
657 fn compute_biome_blending(&self, map: &mut BiomeMap) {
660 let w = map.width;
661 let h = map.height;
662 let biomes_snap = map.biomes.clone();
663
664 for y in 0..h {
665 for x in 0..w {
666 let center = biomes_snap[y * w + x];
667 let mut blend_map: std::collections::HashMap<BiomeKindKey, f32> =
668 std::collections::HashMap::new();
669
670 let mut total = 0.0f32;
672 for dy in -1i64..=1 {
673 for dx in -1i64..=1 {
674 let nx = x as i64 + dx;
675 let ny = y as i64 + dy;
676 let biome = if nx < 0 || ny < 0 || nx >= w as i64 || ny >= h as i64 {
677 center
678 } else {
679 biomes_snap[ny as usize * w + nx as usize]
680 };
681 let weight = if dx == 0 && dy == 0 { 4.0 }
682 else if dx == 0 || dy == 0 { 2.0 }
683 else { 1.0 };
684 *blend_map.entry(BiomeKindKey(biome)).or_insert(0.0) += weight;
685 total += weight;
686 }
687 }
688
689 let mut entries: Vec<(BiomeKind, f32)> = blend_map
691 .into_iter()
692 .map(|(k, w)| (k.0, w / total))
693 .collect();
694 entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
695 entries.truncate(4);
696
697 let sum: f32 = entries.iter().map(|e| e.1).sum();
699 let mut blend = BiomeBlend::default();
700 blend.count = entries.len();
701 for (i, (biome, weight)) in entries.into_iter().enumerate() {
702 blend.entries[i] = (biome, if sum > 0.0 { weight / sum } else { 0.0 });
703 }
704
705 map.blend[y * w + x] = blend;
706 }
707 }
708 }
709}
710
711#[derive(PartialEq, Eq, Hash)]
714struct BiomeKindKey(BiomeKind);
715
716#[derive(Clone, Debug)]
720pub struct VegetationDensityMap {
721 pub width: usize,
722 pub height: usize,
723 pub trees: Vec<f32>,
725 pub grass: Vec<f32>,
727 pub shrubs: Vec<f32>,
729 pub rocks: Vec<f32>,
731}
732
733impl VegetationDensityMap {
734 pub fn generate(biome_map: &BiomeMap, heightmap: &HeightMap) -> Self {
735 let w = biome_map.width;
736 let h = biome_map.height;
737 let size = w * h;
738
739 let mut trees = vec![0.0f32; size];
740 let mut grass = vec![0.0f32; size];
741 let mut shrubs = vec![0.0f32; size];
742 let mut rocks = vec![0.0f32; size];
743
744 for y in 0..h {
745 for x in 0..w {
746 let i = y * w + x;
747 let slope = heightmap.slope_at(x, y);
748 let biome = biome_map.get(x, y);
749 let (t, g, s, r) = vegetation_densities(biome, slope);
750 trees[i] = t;
751 grass[i] = g;
752 shrubs[i] = s;
753 rocks[i] = r;
754 }
755 }
756
757 Self { width: w, height: h, trees, grass, shrubs, rocks }
758 }
759
760 pub fn tree_density_at(&self, x: usize, y: usize) -> f32 {
761 self.trees[y * self.width + x]
762 }
763
764 pub fn grass_density_at(&self, x: usize, y: usize) -> f32 {
765 self.grass[y * self.width + x]
766 }
767}
768
769fn vegetation_densities(biome: BiomeKind, slope: f32) -> (f32, f32, f32, f32) {
771 let slope_pen = (slope * 3.0).clamp(0.0, 1.0);
773 let rock_bonus = slope_pen * 0.8;
774
775 let (t, g, s, r) = match biome {
776 BiomeKind::TropicalRainforest => (0.90, 0.70, 0.40, 0.05),
777 BiomeKind::TropicalDryForest => (0.65, 0.50, 0.30, 0.10),
778 BiomeKind::Savanna => (0.15, 0.80, 0.20, 0.10),
779 BiomeKind::TemperateRainforest => (0.85, 0.60, 0.35, 0.08),
780 BiomeKind::TemperateSeasonalForest => (0.70, 0.50, 0.25, 0.10),
781 BiomeKind::TemperateGrassland => (0.05, 0.90, 0.15, 0.08),
782 BiomeKind::BorealForest => (0.75, 0.20, 0.20, 0.12),
783 BiomeKind::Shrubland => (0.10, 0.50, 0.70, 0.15),
784 BiomeKind::Tundra => (0.00, 0.30, 0.10, 0.25),
785 BiomeKind::Alpine => (0.00, 0.10, 0.05, 0.70),
786 BiomeKind::Glacier => (0.00, 0.00, 0.00, 0.15),
787 BiomeKind::Desert | BiomeKind::HotDesert => (0.00, 0.05, 0.08, 0.20),
788 BiomeKind::SemiArid => (0.03, 0.25, 0.25, 0.18),
789 BiomeKind::Wetland => (0.20, 0.80, 0.30, 0.02),
790 BiomeKind::Mangrove => (0.80, 0.30, 0.10, 0.02),
791 BiomeKind::Beach => (0.00, 0.10, 0.05, 0.20),
792 BiomeKind::Ocean | BiomeKind::ShallowWater |
793 BiomeKind::Lake | BiomeKind::River => (0.00, 0.00, 0.00, 0.00),
794 BiomeKind::Volcano => (0.00, 0.00, 0.00, 0.80),
795 };
796
797 let t = (t * (1.0 - slope_pen)).clamp(0.0, 1.0);
798 let g = (g * (1.0 - slope_pen * 0.5)).clamp(0.0, 1.0);
799 let s = (s * (1.0 - slope_pen * 0.7)).clamp(0.0, 1.0);
800 let r = (r + rock_bonus).clamp(0.0, 1.0);
801
802 (t, g, s, r)
803}
804
805#[derive(Clone, Copy, Debug, PartialEq, Eq)]
809pub enum SoilType {
810 Bedrock,
811 Granite,
812 Limestone,
813 Sandstone,
814 Basalt,
815 Soil,
816 LoamySoil,
817 ClayRich,
818 SandyLoam,
819 PeatBog,
820 Permafrost,
821 VolcanicAsh,
822}
823
824impl SoilType {
825 pub fn drainage(self) -> f32 {
827 match self {
828 SoilType::Bedrock => 0.9,
829 SoilType::Granite => 0.85,
830 SoilType::Limestone => 0.75,
831 SoilType::Sandstone => 0.80,
832 SoilType::Basalt => 0.70,
833 SoilType::Soil => 0.55,
834 SoilType::LoamySoil => 0.50,
835 SoilType::ClayRich => 0.15,
836 SoilType::SandyLoam => 0.80,
837 SoilType::PeatBog => 0.05,
838 SoilType::Permafrost => 0.02,
839 SoilType::VolcanicAsh => 0.65,
840 }
841 }
842
843 pub fn fertility(self) -> f32 {
845 match self {
846 SoilType::Bedrock => 0.0,
847 SoilType::Granite => 0.05,
848 SoilType::Limestone => 0.30,
849 SoilType::Sandstone => 0.20,
850 SoilType::Basalt => 0.40,
851 SoilType::Soil => 0.70,
852 SoilType::LoamySoil => 0.90,
853 SoilType::ClayRich => 0.60,
854 SoilType::SandyLoam => 0.50,
855 SoilType::PeatBog => 0.55,
856 SoilType::Permafrost => 0.10,
857 SoilType::VolcanicAsh => 0.80,
858 }
859 }
860}
861
862pub struct SoilAssigner;
864
865impl SoilAssigner {
866 pub fn assign(biome_map: &BiomeMap, heightmap: &HeightMap) -> Vec<SoilType> {
867 let w = heightmap.width;
868 let h = heightmap.height;
869 let mut out = Vec::with_capacity(w * h);
870
871 for y in 0..h {
872 for x in 0..w {
873 let alt = heightmap.get(x, y);
874 let biome = biome_map.get(x, y);
875 let slope = heightmap.slope_at(x, y);
876
877 let soil = Self::classify(biome, alt, slope);
878 out.push(soil);
879 }
880 }
881
882 out
883 }
884
885 fn classify(biome: BiomeKind, altitude: f32, slope: f32) -> SoilType {
886 if altitude > 0.90 { return SoilType::Bedrock; }
887 if slope > 0.60 { return SoilType::Granite; }
888
889 match biome {
890 BiomeKind::Glacier => SoilType::Permafrost,
891 BiomeKind::Alpine => SoilType::Bedrock,
892 BiomeKind::Tundra => SoilType::Permafrost,
893 BiomeKind::BorealForest => SoilType::PeatBog,
894 BiomeKind::TemperateRainforest |
895 BiomeKind::TropicalRainforest => SoilType::LoamySoil,
896 BiomeKind::TemperateSeasonalForest |
897 BiomeKind::TropicalDryForest => SoilType::Soil,
898 BiomeKind::TemperateGrassland |
899 BiomeKind::Savanna => SoilType::LoamySoil,
900 BiomeKind::Shrubland => SoilType::SandyLoam,
901 BiomeKind::Desert |
902 BiomeKind::HotDesert => SoilType::Sandstone,
903 BiomeKind::SemiArid => SoilType::SandyLoam,
904 BiomeKind::Wetland => SoilType::PeatBog,
905 BiomeKind::Mangrove => SoilType::ClayRich,
906 BiomeKind::Beach => SoilType::Sandstone,
907 BiomeKind::Volcano => SoilType::VolcanicAsh,
908 BiomeKind::Ocean |
909 BiomeKind::ShallowWater => SoilType::Basalt,
910 BiomeKind::Lake |
911 BiomeKind::River => SoilType::ClayRich,
912 }
913 }
914}
915
916#[derive(Clone, Debug)]
920pub struct CoastlinePoint {
921 pub x: usize,
922 pub y: usize,
923 pub facing: CoastFacing,
924}
925
926#[derive(Clone, Copy, Debug, PartialEq, Eq)]
928pub enum CoastFacing {
929 North, South, East, West,
930}
931
932pub fn detect_coastline(map: &BiomeMap) -> Vec<CoastlinePoint> {
934 let w = map.width;
935 let h = map.height;
936 let mut points = Vec::new();
937
938 for y in 0..h {
939 for x in 0..w {
940 if map.get(x, y).is_water() { continue; }
941 let neighbors = [
943 (x as i64, y as i64 - 1, CoastFacing::North),
944 (x as i64, y as i64 + 1, CoastFacing::South),
945 (x as i64 + 1, y as i64, CoastFacing::East),
946 (x as i64 - 1, y as i64, CoastFacing::West),
947 ];
948 for (nx, ny, facing) in &neighbors {
949 if *nx < 0 || *ny < 0 || *nx >= w as i64 || *ny >= h as i64 { continue; }
950 let nb = map.get(*nx as usize, *ny as usize);
951 if nb == BiomeKind::Ocean || nb == BiomeKind::ShallowWater {
952 points.push(CoastlinePoint { x, y, facing: *facing });
953 break; }
955 }
956 }
957 }
958
959 points
960}
961
962#[derive(Clone, Debug, Default)]
966pub struct BiomeStats {
967 pub ocean_fraction: f32,
968 pub land_fraction: f32,
969 pub forest_fraction: f32,
970 pub desert_fraction: f32,
971 pub unique_biomes: usize,
972}
973
974impl BiomeStats {
975 pub fn compute(map: &BiomeMap) -> Self {
976 let total = (map.width * map.height) as f32;
977 let mut ocean = 0usize;
978 let mut forest = 0usize;
979 let mut desert = 0usize;
980 let mut kinds = std::collections::HashSet::new();
981
982 for &b in &map.biomes {
983 kinds.insert(b as u8);
984 if b.is_water() { ocean += 1; }
985 if b.is_forested() { forest += 1; }
986 if matches!(b, BiomeKind::Desert | BiomeKind::HotDesert |
987 BiomeKind::SemiArid) { desert += 1; }
988 }
989
990 Self {
991 ocean_fraction: ocean as f32 / total,
992 land_fraction: (total as usize - ocean) as f32 / total,
993 forest_fraction: forest as f32 / total,
994 desert_fraction: desert as f32 / total,
995 unique_biomes: kinds.len(),
996 }
997 }
998}
999
1000#[cfg(test)]
1003mod tests {
1004 use super::*;
1005 use crate::terrain::heightmap::DiamondSquare;
1006
1007 fn make_test_heightmap() -> HeightMap {
1008 DiamondSquare::generate(32, 0.5, 42)
1009 }
1010
1011 #[test]
1012 fn test_whittaker_hot_wet() {
1013 let cell = ClimateCell {
1014 temperature: 0.90,
1015 moisture: 0.90,
1016 altitude: 0.40,
1017 continentality: 0.10,
1018 };
1019 assert_eq!(WhittakerClassifier::classify(&cell), BiomeKind::TropicalRainforest);
1020 }
1021
1022 #[test]
1023 fn test_whittaker_cold_dry() {
1024 let cell = ClimateCell {
1025 temperature: 0.15,
1026 moisture: 0.10,
1027 altitude: 0.30,
1028 continentality: 0.80,
1029 };
1030 assert_eq!(WhittakerClassifier::classify(&cell), BiomeKind::Tundra);
1031 }
1032
1033 #[test]
1034 fn test_whittaker_high_altitude() {
1035 let cell = ClimateCell {
1036 temperature: 0.50,
1037 moisture: 0.50,
1038 altitude: 0.95,
1039 continentality: 0.50,
1040 };
1041 assert_eq!(WhittakerClassifier::classify(&cell), BiomeKind::Glacier);
1042 }
1043
1044 #[test]
1045 fn test_biome_placer_coverage() {
1046 let hm = make_test_heightmap();
1047 let placer = BiomePlacer::new(1234);
1048 let bmap = placer.generate(&hm);
1049 assert_eq!(bmap.biomes.len(), hm.data.len());
1050 }
1051
1052 #[test]
1053 fn test_biome_blend_weights_sum_to_1() {
1054 let hm = make_test_heightmap();
1055 let placer = BiomePlacer::new(9999);
1056 let bmap = placer.generate(&hm);
1057 for blend in &bmap.blend {
1058 let sum: f32 = blend.entries[..blend.count].iter().map(|e| e.1).sum();
1059 assert!((sum - 1.0).abs() < 1e-4 || blend.count == 0,
1060 "blend weights don't sum to 1: {sum}");
1061 }
1062 }
1063
1064 #[test]
1065 fn test_vegetation_density_water_is_zero() {
1066 let (t, g, s, r) = vegetation_densities(BiomeKind::Ocean, 0.0);
1067 assert_eq!(t, 0.0);
1068 assert_eq!(g, 0.0);
1069 assert_eq!(s, 0.0);
1070 assert_eq!(r, 0.0);
1071 }
1072
1073 #[test]
1074 fn test_soil_assigner_count() {
1075 let hm = make_test_heightmap();
1076 let placer = BiomePlacer::new(0);
1077 let bmap = placer.generate(&hm);
1078 let soils = SoilAssigner::assign(&bmap, &hm);
1079 assert_eq!(soils.len(), hm.data.len());
1080 }
1081
1082 #[test]
1083 fn test_coastline_detection() {
1084 let hm = make_test_heightmap();
1085 let placer = BiomePlacer::new(5555);
1086 let bmap = placer.generate(&hm);
1087 let coast = detect_coastline(&bmap);
1088 let has_ocean = bmap.biomes.iter().any(|b| *b == BiomeKind::Ocean);
1090 let has_land = bmap.biomes.iter().any(|b| !b.is_water());
1091 if has_ocean && has_land {
1092 assert!(!coast.is_empty(), "expected coastline points");
1093 }
1094 }
1095
1096 #[test]
1097 fn test_biome_stats() {
1098 let hm = make_test_heightmap();
1099 let placer = BiomePlacer::new(42);
1100 let bmap = placer.generate(&hm);
1101 let stats = BiomeStats::compute(&bmap);
1102 assert!(stats.land_fraction + stats.ocean_fraction > 0.0);
1103 assert!(stats.unique_biomes > 0);
1104 }
1105}