1use glam::Vec3;
8use crate::terrain::heightmap::HeightMap;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum BiomeType {
15 Ocean,
16 DeepOcean,
17 Beach,
18 Desert,
19 Savanna,
20 Grassland,
21 Shrubland,
22 TemperateForest,
23 TropicalForest,
24 Boreal,
25 Taiga,
26 Tundra,
27 Arctic,
28 Mountain,
29 AlpineGlacier,
30 Swamp,
31 Mangrove,
32 Volcanic,
33 Badlands,
34 Mushroom,
35}
36
37impl BiomeType {
38 pub fn name(self) -> &'static str {
40 match self {
41 BiomeType::Ocean => "Ocean",
42 BiomeType::DeepOcean => "Deep Ocean",
43 BiomeType::Beach => "Beach",
44 BiomeType::Desert => "Desert",
45 BiomeType::Savanna => "Savanna",
46 BiomeType::Grassland => "Grassland",
47 BiomeType::Shrubland => "Shrubland",
48 BiomeType::TemperateForest => "Temperate Forest",
49 BiomeType::TropicalForest => "Tropical Forest",
50 BiomeType::Boreal => "Boreal Forest",
51 BiomeType::Taiga => "Taiga",
52 BiomeType::Tundra => "Tundra",
53 BiomeType::Arctic => "Arctic",
54 BiomeType::Mountain => "Mountain",
55 BiomeType::AlpineGlacier => "Alpine Glacier",
56 BiomeType::Swamp => "Swamp",
57 BiomeType::Mangrove => "Mangrove",
58 BiomeType::Volcanic => "Volcanic",
59 BiomeType::Badlands => "Badlands",
60 BiomeType::Mushroom => "Mushroom Island",
61 }
62 }
63
64 pub fn is_aquatic(self) -> bool {
66 matches!(self, BiomeType::Ocean | BiomeType::DeepOcean | BiomeType::Swamp | BiomeType::Mangrove)
67 }
68
69 pub fn is_cold(self) -> bool {
71 matches!(self, BiomeType::Tundra | BiomeType::Arctic | BiomeType::AlpineGlacier | BiomeType::Taiga)
72 }
73
74 pub fn has_trees(self) -> bool {
76 matches!(self,
77 BiomeType::TemperateForest | BiomeType::TropicalForest |
78 BiomeType::Boreal | BiomeType::Taiga | BiomeType::Swamp |
79 BiomeType::Mangrove | BiomeType::Mushroom
80 )
81 }
82
83 pub fn index(self) -> usize {
85 self as usize
86 }
87}
88
89#[derive(Clone, Copy, Debug, Default)]
93pub struct BiomeParams {
94 pub temperature: f32,
96 pub humidity: f32,
98 pub altitude: f32,
100 pub slope: f32,
102 pub coast_distance: f32,
104 pub volcanic: bool,
106}
107
108pub struct BiomeClassifier;
115
116impl BiomeClassifier {
117 pub fn classify(p: &BiomeParams) -> BiomeType {
119 if p.volcanic { return BiomeType::Volcanic; }
121 if p.altitude < 0.05 { return if p.altitude < 0.02 { BiomeType::DeepOcean } else { BiomeType::Ocean }; }
122 if p.altitude < 0.1 && p.coast_distance < 0.05 { return BiomeType::Beach; }
123 if p.altitude < 0.1 && p.humidity > 0.7 && p.temperature > 0.5 { return BiomeType::Mangrove; }
124
125 if p.altitude > 0.85 {
127 if p.temperature < 0.3 || p.altitude > 0.95 { return BiomeType::AlpineGlacier; }
128 return BiomeType::Mountain;
129 }
130 if p.altitude > 0.7 {
131 if p.slope > 0.5 { return BiomeType::Mountain; }
132 if p.temperature < 0.2 { return BiomeType::AlpineGlacier; }
133 }
134
135 if p.temperature < 0.1 { return BiomeType::Arctic; }
137 if p.temperature < 0.25 {
138 if p.humidity < 0.3 { return BiomeType::Tundra; }
139 return BiomeType::Taiga;
140 }
141 if p.temperature < 0.4 {
142 if p.humidity > 0.5 { return BiomeType::Boreal; }
143 return BiomeType::Tundra;
144 }
145
146 if p.humidity > 0.75 {
149 if p.temperature > 0.65 { return BiomeType::TropicalForest; }
150 if p.temperature > 0.45 { return BiomeType::TemperateForest; }
151 return BiomeType::Boreal;
152 }
153
154 if p.humidity > 0.55 {
155 if p.temperature > 0.65 {
156 if p.altitude < 0.15 && p.coast_distance < 0.1 { return BiomeType::Mangrove; }
157 return BiomeType::TropicalForest;
158 }
159 if p.temperature > 0.45 {
160 if p.humidity > 0.65 && p.altitude < 0.15 { return BiomeType::Swamp; }
161 return BiomeType::TemperateForest;
162 }
163 return BiomeType::Boreal;
164 }
165
166 if p.humidity > 0.35 {
167 if p.temperature > 0.65 { return BiomeType::Savanna; }
168 if p.temperature > 0.45 { return BiomeType::Grassland; }
169 return BiomeType::Shrubland;
170 }
171
172 if p.humidity > 0.2 {
173 if p.temperature > 0.55 { return BiomeType::Savanna; }
174 if p.temperature > 0.4 { return BiomeType::Grassland; }
175 return BiomeType::Shrubland;
176 }
177
178 if p.humidity < 0.15 {
180 if p.temperature > 0.5 { return BiomeType::Desert; }
181 if p.temperature > 0.3 { return BiomeType::Badlands; }
182 return BiomeType::Tundra;
183 }
184
185 if p.temperature > 0.6 { return BiomeType::Savanna; }
187 if p.temperature > 0.4 { return BiomeType::Shrubland; }
188 BiomeType::Tundra
189 }
190
191 pub fn classify_blended(p: &BiomeParams) -> [(BiomeType, f32); 4] {
194 let base = Self::classify(p);
195 let p_warm = BiomeParams { temperature: p.temperature + 0.05, ..*p };
197 let p_wet = BiomeParams { humidity: p.humidity + 0.05, ..*p };
198 let p_high = BiomeParams { altitude: p.altitude + 0.05, ..*p };
199 let b1 = Self::classify(&p_warm);
200 let b2 = Self::classify(&p_wet);
201 let b3 = Self::classify(&p_high);
202 [
203 (base, 0.7),
204 (b1, if b1 != base { 0.1 } else { 0.0 }),
205 (b2, if b2 != base { 0.1 } else { 0.0 }),
206 (b3, if b3 != base { 0.1 } else { 0.0 }),
207 ]
208 }
209}
210
211pub struct ClimateSimulator {
218 pub latitude_range: (f32, f32),
220 pub base_temperature: f32,
222 pub precipitation_scale: f32,
224 pub wind_direction: (f32, f32),
226}
227
228impl Default for ClimateSimulator {
229 fn default() -> Self {
230 Self {
231 latitude_range: (-60.0, 60.0),
232 base_temperature: 0.5,
233 precipitation_scale: 1.0,
234 wind_direction: (1.0, 0.0),
235 }
236 }
237}
238
239impl ClimateSimulator {
240 pub fn new() -> Self { Self::default() }
241
242 pub fn temperature(&self, nx: f32, ny: f32, altitude: f32) -> f32 {
244 let (lat_s, lat_n) = self.latitude_range;
246 let lat = lat_s + ny * (lat_n - lat_s);
247 let lat_factor = (lat.to_radians().cos()).powf(0.5).clamp(0.0, 1.0);
248
249 let altitude_cooling = altitude * 0.5;
251
252 let hadley_bonus = if lat.abs() < 30.0 {
254 (1.0 - lat.abs() / 30.0) * 0.1
255 } else {
256 0.0
257 };
258
259 (self.base_temperature + lat_factor * 0.4 + hadley_bonus - altitude_cooling)
260 .clamp(0.0, 1.0)
261 }
262
263 pub fn precipitation(
265 &self,
266 nx: f32,
267 ny: f32,
268 altitude: f32,
269 heightmap: &HeightMap,
270 ) -> f32 {
271 let w = heightmap.width as f32;
272 let h = heightmap.height as f32;
273 let x = nx * w;
274 let y = ny * h;
275
276 let (lat_s, lat_n) = self.latitude_range;
278 let lat = lat_s + ny * (lat_n - lat_s);
279 let base_precip = {
280 let p1 = (-(lat / 10.0).powi(2)).exp(); let p2 = (-(((lat.abs() - 55.0) / 15.0)).powi(2)).exp(); let desert_suppress = if lat.abs() > 25.0 && lat.abs() < 35.0 { 0.5 } else { 1.0 };
285 (p1 * 0.6 + p2 * 0.4) * desert_suppress
286 };
287
288 let wind_x = self.wind_direction.0;
290 let wind_y = self.wind_direction.1;
291 let upwind_x = (x - wind_x * 20.0).clamp(0.0, w - 1.0);
292 let upwind_y = (y - wind_y * 20.0).clamp(0.0, h - 1.0);
293 let upwind_h = heightmap.sample_bilinear(upwind_x, upwind_y);
294 let orographic = if altitude > upwind_h + 0.05 {
295 0.2 * ((altitude - upwind_h) / 0.3).clamp(0.0, 1.0)
297 } else if altitude < upwind_h - 0.05 {
298 -0.3 * ((upwind_h - altitude) / 0.3).clamp(0.0, 1.0)
300 } else {
301 0.0
302 };
303
304 let coast_bonus = (1.0 - Self::coast_distance(heightmap, x as usize, y as usize)) * 0.15;
306
307 (base_precip * self.precipitation_scale + orographic + coast_bonus)
308 .clamp(0.0, 1.0)
309 }
310
311 pub fn ocean_current_effect(&self, nx: f32, ny: f32) -> f32 {
313 let (lat_s, lat_n) = self.latitude_range;
314 let lat = lat_s + ny * (lat_n - lat_s);
315 let warm_current = if nx > 0.5 && lat.abs() < 40.0 { 0.1 } else { 0.0 };
318 let cold_current = if nx < 0.2 && lat.abs() > 20.0 { -0.08 } else { 0.0 };
319 warm_current + cold_current
320 }
321
322 fn coast_distance(heightmap: &HeightMap, x: usize, y: usize) -> f32 {
324 let sea_level = 0.1;
325 let is_land = heightmap.get(x, y) > sea_level;
326 let max_search = 32usize;
327 for r in 0..max_search {
328 for dy in -(r as i32)..=(r as i32) {
329 for dx in -(r as i32)..=(r as i32) {
330 if dx.abs() != r as i32 && dy.abs() != r as i32 { continue; }
331 let nx2 = x as i32 + dx;
332 let ny2 = y as i32 + dy;
333 if nx2 < 0 || nx2 >= heightmap.width as i32 || ny2 < 0 || ny2 >= heightmap.height as i32 { continue; }
334 let other_land = heightmap.get(nx2 as usize, ny2 as usize) > sea_level;
335 if other_land != is_land {
336 return r as f32 / max_search as f32;
337 }
338 }
339 }
340 }
341 1.0
342 }
343
344 pub fn simulate(&self, heightmap: &HeightMap) -> ClimateMap {
346 let w = heightmap.width;
347 let h = heightmap.height;
348 let mut temperature = HeightMap::new(w, h);
349 let mut humidity = HeightMap::new(w, h);
350 for y in 0..h {
351 for x in 0..w {
352 let nx = x as f32 / w as f32;
353 let ny = y as f32 / h as f32;
354 let alt = heightmap.get(x, y);
355 let t = self.temperature(nx, ny, alt)
356 + self.ocean_current_effect(nx, ny);
357 let p = self.precipitation(nx, ny, alt, heightmap);
358 temperature.set(x, y, t.clamp(0.0, 1.0));
359 humidity.set(x, y, p.clamp(0.0, 1.0));
360 }
361 }
362 temperature.blur(2);
364 humidity.blur(2);
365 ClimateMap { temperature, humidity }
366 }
367}
368
369#[derive(Clone, Debug)]
371pub struct ClimateMap {
372 pub temperature: HeightMap,
373 pub humidity: HeightMap,
374}
375
376#[derive(Clone, Debug)]
380pub struct BiomeMap {
381 pub width: usize,
382 pub height: usize,
383 pub biomes: Vec<BiomeType>,
384}
385
386impl BiomeMap {
387 pub fn new(width: usize, height: usize, biomes: Vec<BiomeType>) -> Self {
389 assert_eq!(biomes.len(), width * height);
390 Self { width, height, biomes }
391 }
392
393 pub fn from_heightmap(heightmap: &HeightMap, climate: &ClimateMap) -> Self {
395 let w = heightmap.width;
396 let h = heightmap.height;
397 let slope_map = heightmap.slope_map();
398 let mut biomes = Vec::with_capacity(w * h);
399
400 for y in 0..h {
401 for x in 0..w {
402 let altitude = heightmap.get(x, y);
403 let temperature = climate.temperature.get(x, y);
404 let humidity = climate.humidity.get(x, y);
405 let slope = slope_map.get(x, y);
406 let coast_dist = ClimateSimulator::coast_distance(heightmap, x, y);
407
408 let volcanic = altitude > 0.75 && slope > 0.7 && temperature > 0.6;
410
411 let params = BiomeParams {
412 temperature,
413 humidity,
414 altitude,
415 slope,
416 coast_distance: coast_dist,
417 volcanic,
418 };
419 biomes.push(BiomeClassifier::classify(¶ms));
420 }
421 }
422 Self { width: w, height: h, biomes }
423 }
424
425 pub fn get(&self, x: usize, y: usize) -> BiomeType {
427 if x < self.width && y < self.height {
428 self.biomes[y * self.width + x]
429 } else {
430 BiomeType::Ocean
431 }
432 }
433
434 pub fn blend_weights(&self, x: f32, z: f32) -> Vec<(BiomeType, f32)> {
437 let cx = x.clamp(0.0, (self.width - 1) as f32);
438 let cz = z.clamp(0.0, (self.height - 1) as f32);
439 let x0 = cx.floor() as usize;
440 let z0 = cz.floor() as usize;
441 let x1 = (x0 + 1).min(self.width - 1);
442 let z1 = (z0 + 1).min(self.height - 1);
443 let tx = cx - x0 as f32;
444 let tz = cz - z0 as f32;
445
446 let b00 = self.get(x0, z0);
447 let b10 = self.get(x1, z0);
448 let b01 = self.get(x0, z1);
449 let b11 = self.get(x1, z1);
450
451 let w00 = (1.0 - tx) * (1.0 - tz);
452 let w10 = tx * (1.0 - tz);
453 let w01 = (1.0 - tx) * tz;
454 let w11 = tx * tz;
455
456 let mut result: Vec<(BiomeType, f32)> = Vec::new();
458 for (b, w) in [(b00, w00), (b10, w10), (b01, w01), (b11, w11)] {
459 if let Some(entry) = result.iter_mut().find(|(bt, _)| *bt == b) {
460 entry.1 += w;
461 } else {
462 result.push((b, w));
463 }
464 }
465 result
466 }
467}
468
469#[derive(Clone, Copy, Debug, Default)]
473pub struct VegetationDensity {
474 pub tree_density: f32,
476 pub grass_density: f32,
478 pub rock_density: f32,
480 pub shrub_density: f32,
482 pub flower_density: f32,
484}
485
486impl VegetationDensity {
487 pub fn for_biome(biome: BiomeType) -> Self {
489 match biome {
490 BiomeType::Ocean | BiomeType::DeepOcean => Self::default(),
491 BiomeType::Beach => Self {
492 grass_density: 0.05, rock_density: 0.1,
493 ..Default::default()
494 },
495 BiomeType::Desert => Self {
496 tree_density: 0.02, rock_density: 0.3, shrub_density: 0.05,
497 ..Default::default()
498 },
499 BiomeType::Savanna => Self {
500 tree_density: 0.1, grass_density: 0.7, shrub_density: 0.1,
501 flower_density: 0.05, ..Default::default()
502 },
503 BiomeType::Grassland => Self {
504 tree_density: 0.05, grass_density: 0.9,
505 flower_density: 0.15, rock_density: 0.05, ..Default::default()
506 },
507 BiomeType::Shrubland => Self {
508 tree_density: 0.1, grass_density: 0.4, shrub_density: 0.6,
509 rock_density: 0.1, ..Default::default()
510 },
511 BiomeType::TemperateForest => Self {
512 tree_density: 0.7, grass_density: 0.3, shrub_density: 0.2,
513 flower_density: 0.1, rock_density: 0.05,
514 },
515 BiomeType::TropicalForest => Self {
516 tree_density: 0.9, grass_density: 0.2, shrub_density: 0.4,
517 flower_density: 0.3, rock_density: 0.02,
518 },
519 BiomeType::Boreal => Self {
520 tree_density: 0.6, grass_density: 0.1, shrub_density: 0.15,
521 rock_density: 0.1, ..Default::default()
522 },
523 BiomeType::Taiga => Self {
524 tree_density: 0.5, grass_density: 0.05, shrub_density: 0.1,
525 rock_density: 0.15, ..Default::default()
526 },
527 BiomeType::Tundra => Self {
528 tree_density: 0.01, grass_density: 0.3, shrub_density: 0.15,
529 rock_density: 0.3, flower_density: 0.05,
530 },
531 BiomeType::Arctic => Self {
532 rock_density: 0.4, ..Default::default()
533 },
534 BiomeType::Mountain => Self {
535 tree_density: 0.15, grass_density: 0.2, rock_density: 0.6,
536 shrub_density: 0.1, ..Default::default()
537 },
538 BiomeType::AlpineGlacier => Self {
539 rock_density: 0.2, ..Default::default()
540 },
541 BiomeType::Swamp => Self {
542 tree_density: 0.5, grass_density: 0.4, shrub_density: 0.3,
543 flower_density: 0.05, rock_density: 0.01,
544 },
545 BiomeType::Mangrove => Self {
546 tree_density: 0.6, grass_density: 0.1, shrub_density: 0.2,
547 ..Default::default()
548 },
549 BiomeType::Volcanic => Self {
550 rock_density: 0.8, ..Default::default()
551 },
552 BiomeType::Badlands => Self {
553 grass_density: 0.05, rock_density: 0.5, shrub_density: 0.05,
554 ..Default::default()
555 },
556 BiomeType::Mushroom => Self {
557 tree_density: 0.05, grass_density: 0.6, shrub_density: 0.2,
558 flower_density: 0.4, rock_density: 0.05,
559 },
560 }
561 }
562}
563
564#[derive(Clone, Copy, Debug)]
568pub struct BiomeColor {
569 pub ground: Vec3,
571 pub grass: Vec3,
573 pub sky: Vec3,
575 pub water: Vec3,
577 pub rock: Vec3,
579}
580
581impl BiomeColor {
582 pub fn for_biome(biome: BiomeType) -> Self {
584 match biome {
585 BiomeType::Ocean => Self {
586 ground: Vec3::new(0.05, 0.1, 0.3),
587 grass: Vec3::new(0.0, 0.3, 0.5),
588 sky: Vec3::new(0.4, 0.65, 0.9),
589 water: Vec3::new(0.0, 0.2, 0.8),
590 rock: Vec3::new(0.3, 0.3, 0.4),
591 },
592 BiomeType::DeepOcean => Self {
593 ground: Vec3::new(0.02, 0.04, 0.2),
594 grass: Vec3::new(0.0, 0.1, 0.3),
595 sky: Vec3::new(0.3, 0.5, 0.8),
596 water: Vec3::new(0.0, 0.1, 0.6),
597 rock: Vec3::new(0.2, 0.2, 0.3),
598 },
599 BiomeType::Beach => Self {
600 ground: Vec3::new(0.87, 0.80, 0.55),
601 grass: Vec3::new(0.7, 0.75, 0.3),
602 sky: Vec3::new(0.5, 0.75, 0.95),
603 water: Vec3::new(0.1, 0.5, 0.9),
604 rock: Vec3::new(0.6, 0.55, 0.45),
605 },
606 BiomeType::Desert => Self {
607 ground: Vec3::new(0.85, 0.65, 0.3),
608 grass: Vec3::new(0.7, 0.6, 0.25),
609 sky: Vec3::new(0.9, 0.75, 0.45),
610 water: Vec3::new(0.3, 0.5, 0.8),
611 rock: Vec3::new(0.75, 0.55, 0.35),
612 },
613 BiomeType::Savanna => Self {
614 ground: Vec3::new(0.75, 0.6, 0.25),
615 grass: Vec3::new(0.7, 0.65, 0.2),
616 sky: Vec3::new(0.7, 0.8, 0.9),
617 water: Vec3::new(0.2, 0.5, 0.8),
618 rock: Vec3::new(0.65, 0.55, 0.4),
619 },
620 BiomeType::Grassland => Self {
621 ground: Vec3::new(0.45, 0.5, 0.2),
622 grass: Vec3::new(0.35, 0.6, 0.15),
623 sky: Vec3::new(0.5, 0.7, 0.95),
624 water: Vec3::new(0.15, 0.45, 0.8),
625 rock: Vec3::new(0.5, 0.5, 0.45),
626 },
627 BiomeType::Shrubland => Self {
628 ground: Vec3::new(0.5, 0.45, 0.25),
629 grass: Vec3::new(0.4, 0.5, 0.2),
630 sky: Vec3::new(0.55, 0.7, 0.9),
631 water: Vec3::new(0.1, 0.4, 0.75),
632 rock: Vec3::new(0.55, 0.5, 0.4),
633 },
634 BiomeType::TemperateForest => Self {
635 ground: Vec3::new(0.3, 0.35, 0.15),
636 grass: Vec3::new(0.25, 0.55, 0.15),
637 sky: Vec3::new(0.45, 0.65, 0.85),
638 water: Vec3::new(0.1, 0.35, 0.7),
639 rock: Vec3::new(0.45, 0.45, 0.4),
640 },
641 BiomeType::TropicalForest => Self {
642 ground: Vec3::new(0.2, 0.3, 0.1),
643 grass: Vec3::new(0.15, 0.55, 0.1),
644 sky: Vec3::new(0.5, 0.7, 0.75),
645 water: Vec3::new(0.05, 0.4, 0.6),
646 rock: Vec3::new(0.35, 0.4, 0.3),
647 },
648 BiomeType::Boreal => Self {
649 ground: Vec3::new(0.3, 0.35, 0.2),
650 grass: Vec3::new(0.2, 0.45, 0.2),
651 sky: Vec3::new(0.55, 0.65, 0.8),
652 water: Vec3::new(0.1, 0.3, 0.65),
653 rock: Vec3::new(0.4, 0.42, 0.38),
654 },
655 BiomeType::Taiga => Self {
656 ground: Vec3::new(0.35, 0.35, 0.25),
657 grass: Vec3::new(0.25, 0.4, 0.25),
658 sky: Vec3::new(0.6, 0.65, 0.8),
659 water: Vec3::new(0.1, 0.3, 0.6),
660 rock: Vec3::new(0.45, 0.45, 0.4),
661 },
662 BiomeType::Tundra => Self {
663 ground: Vec3::new(0.55, 0.5, 0.4),
664 grass: Vec3::new(0.5, 0.55, 0.3),
665 sky: Vec3::new(0.7, 0.75, 0.85),
666 water: Vec3::new(0.1, 0.3, 0.6),
667 rock: Vec3::new(0.55, 0.52, 0.48),
668 },
669 BiomeType::Arctic => Self {
670 ground: Vec3::new(0.9, 0.92, 0.95),
671 grass: Vec3::new(0.85, 0.88, 0.92),
672 sky: Vec3::new(0.7, 0.8, 0.95),
673 water: Vec3::new(0.6, 0.75, 0.9),
674 rock: Vec3::new(0.6, 0.62, 0.65),
675 },
676 BiomeType::Mountain => Self {
677 ground: Vec3::new(0.5, 0.48, 0.44),
678 grass: Vec3::new(0.35, 0.45, 0.25),
679 sky: Vec3::new(0.55, 0.65, 0.85),
680 water: Vec3::new(0.1, 0.3, 0.7),
681 rock: Vec3::new(0.55, 0.52, 0.48),
682 },
683 BiomeType::AlpineGlacier => Self {
684 ground: Vec3::new(0.85, 0.9, 0.95),
685 grass: Vec3::new(0.8, 0.85, 0.9),
686 sky: Vec3::new(0.65, 0.75, 0.95),
687 water: Vec3::new(0.7, 0.85, 0.95),
688 rock: Vec3::new(0.6, 0.62, 0.65),
689 },
690 BiomeType::Swamp => Self {
691 ground: Vec3::new(0.25, 0.3, 0.15),
692 grass: Vec3::new(0.2, 0.4, 0.15),
693 sky: Vec3::new(0.45, 0.55, 0.65),
694 water: Vec3::new(0.1, 0.2, 0.25),
695 rock: Vec3::new(0.3, 0.32, 0.28),
696 },
697 BiomeType::Mangrove => Self {
698 ground: Vec3::new(0.3, 0.35, 0.2),
699 grass: Vec3::new(0.2, 0.5, 0.15),
700 sky: Vec3::new(0.5, 0.65, 0.8),
701 water: Vec3::new(0.1, 0.3, 0.5),
702 rock: Vec3::new(0.35, 0.38, 0.3),
703 },
704 BiomeType::Volcanic => Self {
705 ground: Vec3::new(0.15, 0.1, 0.08),
706 grass: Vec3::new(0.2, 0.18, 0.1),
707 sky: Vec3::new(0.5, 0.35, 0.25),
708 water: Vec3::new(0.8, 0.4, 0.05),
709 rock: Vec3::new(0.1, 0.08, 0.07),
710 },
711 BiomeType::Badlands => Self {
712 ground: Vec3::new(0.75, 0.45, 0.25),
713 grass: Vec3::new(0.6, 0.45, 0.2),
714 sky: Vec3::new(0.8, 0.65, 0.45),
715 water: Vec3::new(0.25, 0.45, 0.75),
716 rock: Vec3::new(0.7, 0.5, 0.3),
717 },
718 BiomeType::Mushroom => Self {
719 ground: Vec3::new(0.55, 0.3, 0.55),
720 grass: Vec3::new(0.5, 0.2, 0.6),
721 sky: Vec3::new(0.6, 0.5, 0.8),
722 water: Vec3::new(0.4, 0.2, 0.7),
723 rock: Vec3::new(0.45, 0.3, 0.5),
724 },
725 }
726 }
727}
728
729#[derive(Clone, Debug)]
733pub struct TransitionZone {
734 pub biome_a: BiomeType,
735 pub biome_b: BiomeType,
736 pub blend_width: f32,
738 pub sharp_boundary: bool,
740}
741
742impl TransitionZone {
743 pub fn new(biome_a: BiomeType, biome_b: BiomeType, blend_width: f32) -> Self {
744 let sharp = matches!(
745 (biome_a, biome_b),
746 (BiomeType::Grassland, BiomeType::Desert) |
747 (BiomeType::Desert, BiomeType::Grassland) |
748 (BiomeType::Mountain, BiomeType::AlpineGlacier) |
749 (BiomeType::AlpineGlacier, BiomeType::Mountain)
750 );
751 Self { biome_a, biome_b, blend_width, sharp_boundary: sharp }
752 }
753
754 pub fn blend_factor(&self, position: f32) -> f32 {
757 let t = position.clamp(0.0, 1.0);
758 if self.sharp_boundary {
759 if t < 0.5 { 0.0 } else { 1.0 }
760 } else {
761 let x = t * 2.0 - 1.0;
763 0.5 + x * (1.0 - x.abs() * 0.5) * 0.5
764 }
765 }
766}
767
768#[derive(Clone, Copy, Debug)]
772pub struct SeasonFactor {
773 pub vegetation_green: f32,
775 pub autumn_shift: f32,
777 pub snow_cover: f32,
779 pub density_scale: f32,
781}
782
783impl SeasonFactor {
784 pub fn season_factor(biome: BiomeType, month: u32) -> Self {
786 let month = (month % 12) as f32;
787 let summer_t = ((month - 6.0) * std::f32::consts::PI / 6.0).cos() * 0.5 + 0.5;
789 let winter_t = 1.0 - summer_t;
791
792 match biome {
793 BiomeType::TemperateForest | BiomeType::Boreal => Self {
794 vegetation_green: 0.2 + summer_t * 0.8,
795 autumn_shift: if month > 7.0 && month < 11.0 { (month - 7.0) * 0.25 } else { 0.0 },
796 snow_cover: (winter_t - 0.6).max(0.0) * 2.5,
797 density_scale: 0.3 + summer_t * 0.7,
798 },
799 BiomeType::Taiga | BiomeType::Tundra => Self {
800 vegetation_green: 0.1 + summer_t * 0.7,
801 autumn_shift: 0.0,
802 snow_cover: winter_t * 0.9,
803 density_scale: 0.1 + summer_t * 0.6,
804 },
805 BiomeType::Arctic | BiomeType::AlpineGlacier => Self {
806 vegetation_green: summer_t * 0.2,
807 autumn_shift: 0.0,
808 snow_cover: 0.5 + winter_t * 0.5,
809 density_scale: summer_t * 0.15,
810 },
811 BiomeType::Grassland | BiomeType::Savanna => Self {
812 vegetation_green: 0.4 + summer_t * 0.5,
813 autumn_shift: (winter_t - 0.3).max(0.0) * 0.5,
814 snow_cover: (winter_t - 0.8).max(0.0) * 2.0,
815 density_scale: 0.5 + summer_t * 0.5,
816 },
817 BiomeType::Desert | BiomeType::Badlands => Self {
818 vegetation_green: 0.1,
819 autumn_shift: 0.0,
820 snow_cover: 0.0,
821 density_scale: 0.8 + summer_t * 0.2,
822 },
823 BiomeType::TropicalForest | BiomeType::Mangrove | BiomeType::Swamp => Self {
825 vegetation_green: 0.9,
826 autumn_shift: 0.0,
827 snow_cover: 0.0,
828 density_scale: 1.0,
829 },
830 _ => Self {
831 vegetation_green: 0.5 + summer_t * 0.5,
832 autumn_shift: 0.0,
833 snow_cover: winter_t * 0.3,
834 density_scale: 0.6 + summer_t * 0.4,
835 },
836 }
837 }
838}
839
840#[cfg(test)]
843mod tests {
844 use super::*;
845 use crate::terrain::heightmap::FractalNoise;
846
847 #[test]
848 fn test_biome_type_names() {
849 assert_eq!(BiomeType::Desert.name(), "Desert");
850 assert_eq!(BiomeType::TropicalForest.name(), "Tropical Forest");
851 assert_eq!(BiomeType::AlpineGlacier.name(), "Alpine Glacier");
852 }
853
854 #[test]
855 fn test_biome_type_properties() {
856 assert!(BiomeType::Ocean.is_aquatic());
857 assert!(!BiomeType::Desert.is_aquatic());
858 assert!(BiomeType::Arctic.is_cold());
859 assert!(!BiomeType::Desert.is_cold());
860 assert!(BiomeType::TropicalForest.has_trees());
861 assert!(!BiomeType::Arctic.has_trees());
862 }
863
864 #[test]
865 fn test_biome_classifier_desert() {
866 let p = BiomeParams {
867 temperature: 0.8, humidity: 0.1, altitude: 0.3, slope: 0.05,
868 coast_distance: 0.9, volcanic: false,
869 };
870 assert_eq!(BiomeClassifier::classify(&p), BiomeType::Desert);
871 }
872
873 #[test]
874 fn test_biome_classifier_ocean() {
875 let p = BiomeParams {
876 temperature: 0.5, humidity: 0.8, altitude: 0.01, slope: 0.0,
877 coast_distance: 0.0, volcanic: false,
878 };
879 assert!(matches!(
880 BiomeClassifier::classify(&p),
881 BiomeType::Ocean | BiomeType::DeepOcean
882 ));
883 }
884
885 #[test]
886 fn test_biome_classifier_alpine() {
887 let p = BiomeParams {
888 temperature: 0.2, humidity: 0.3, altitude: 0.96, slope: 0.3,
889 coast_distance: 0.8, volcanic: false,
890 };
891 assert_eq!(BiomeClassifier::classify(&p), BiomeType::AlpineGlacier);
892 }
893
894 #[test]
895 fn test_biome_classifier_tropical() {
896 let p = BiomeParams {
897 temperature: 0.9, humidity: 0.9, altitude: 0.4, slope: 0.05,
898 coast_distance: 0.5, volcanic: false,
899 };
900 assert_eq!(BiomeClassifier::classify(&p), BiomeType::TropicalForest);
901 }
902
903 #[test]
904 fn test_biome_classifier_volcanic() {
905 let p = BiomeParams {
906 temperature: 0.7, humidity: 0.2, altitude: 0.8, slope: 0.75,
907 coast_distance: 0.7, volcanic: true,
908 };
909 assert_eq!(BiomeClassifier::classify(&p), BiomeType::Volcanic);
910 }
911
912 #[test]
913 fn test_climate_simulator() {
914 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
915 let sim = ClimateSimulator::default();
916 let climate = sim.simulate(&hm);
917 assert_eq!(climate.temperature.data.len(), 32 * 32);
918 assert_eq!(climate.humidity.data.len(), 32 * 32);
919 assert!(climate.temperature.min_value() >= 0.0);
920 assert!(climate.temperature.max_value() <= 1.0);
921 }
922
923 #[test]
924 fn test_biome_map_from_heightmap() {
925 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
926 let sim = ClimateSimulator::default();
927 let climate = sim.simulate(&hm);
928 let bm = BiomeMap::from_heightmap(&hm, &climate);
929 assert_eq!(bm.biomes.len(), 32 * 32);
930 }
931
932 #[test]
933 fn test_biome_map_blend_weights() {
934 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
935 let sim = ClimateSimulator::default();
936 let climate = sim.simulate(&hm);
937 let bm = BiomeMap::from_heightmap(&hm, &climate);
938 let weights = bm.blend_weights(16.5, 16.5);
939 let total: f32 = weights.iter().map(|(_, w)| w).sum();
940 assert!((total - 1.0).abs() < 1e-4);
941 }
942
943 #[test]
944 fn test_vegetation_density() {
945 let d = VegetationDensity::for_biome(BiomeType::TropicalForest);
946 assert!(d.tree_density > 0.5);
947 let d2 = VegetationDensity::for_biome(BiomeType::Arctic);
948 assert!(d2.tree_density < 0.1);
949 }
950
951 #[test]
952 fn test_biome_colors_all_defined() {
953 let all = [
954 BiomeType::Ocean, BiomeType::DeepOcean, BiomeType::Beach,
955 BiomeType::Desert, BiomeType::Savanna, BiomeType::Grassland,
956 BiomeType::Shrubland, BiomeType::TemperateForest, BiomeType::TropicalForest,
957 BiomeType::Boreal, BiomeType::Taiga, BiomeType::Tundra,
958 BiomeType::Arctic, BiomeType::Mountain, BiomeType::AlpineGlacier,
959 BiomeType::Swamp, BiomeType::Mangrove, BiomeType::Volcanic,
960 BiomeType::Badlands, BiomeType::Mushroom,
961 ];
962 for biome in all {
963 let color = BiomeColor::for_biome(biome);
964 assert!(color.ground.x >= 0.0 && color.ground.x <= 1.0);
966 }
967 }
968
969 #[test]
970 fn test_season_factor() {
971 let summer = SeasonFactor::season_factor(BiomeType::TemperateForest, 6);
972 let winter = SeasonFactor::season_factor(BiomeType::TemperateForest, 0);
973 assert!(summer.vegetation_green > winter.vegetation_green);
974 assert!(winter.snow_cover >= summer.snow_cover);
975 }
976
977 #[test]
978 fn test_transition_zone() {
979 let tz = TransitionZone::new(BiomeType::Grassland, BiomeType::Desert, 10.0);
980 assert!((tz.blend_factor(0.0) - 0.0).abs() < 0.01);
981 let mid = tz.blend_factor(0.5);
982 assert!(mid > 0.0 && mid < 1.0);
983 }
984}
985
986#[derive(Clone, Debug, Default)]
990pub struct BiomeStats {
991 pub counts: [usize; 20],
993 pub total: usize,
995}
996
997impl BiomeStats {
998 pub fn from_map(bm: &BiomeMap) -> Self {
1000 let mut stats = Self::default();
1001 stats.total = bm.biomes.len();
1002 for &b in &bm.biomes {
1003 let idx = b as usize;
1004 if idx < 20 { stats.counts[idx] += 1; }
1005 }
1006 stats
1007 }
1008
1009 pub fn fraction(&self, biome: BiomeType) -> f32 {
1011 if self.total == 0 { return 0.0; }
1012 self.counts[biome as usize] as f32 / self.total as f32
1013 }
1014
1015 pub fn dominant_biome(&self) -> BiomeType {
1017 let idx = self.counts.iter().enumerate()
1018 .max_by_key(|(_, &c)| c)
1019 .map(|(i, _)| i)
1020 .unwrap_or(0);
1021 biome_from_index(idx)
1022 }
1023
1024 pub fn sorted_biomes(&self) -> Vec<(BiomeType, usize)> {
1026 let mut pairs: Vec<(BiomeType, usize)> = self.counts.iter()
1027 .enumerate()
1028 .filter(|(_, &c)| c > 0)
1029 .map(|(i, &c)| (biome_from_index(i), c))
1030 .collect();
1031 pairs.sort_by(|a, b| b.1.cmp(&a.1));
1032 pairs
1033 }
1034
1035 pub fn diversity_index(&self) -> f32 {
1037 if self.total == 0 { return 0.0; }
1038 let n = self.total as f32;
1039 let entropy: f32 = self.counts.iter()
1040 .filter(|&&c| c > 0)
1041 .map(|&c| {
1042 let p = c as f32 / n;
1043 -p * p.ln()
1044 })
1045 .sum();
1046 entropy / (20.0f32).ln()
1048 }
1049}
1050
1051pub fn biome_from_index(idx: usize) -> BiomeType {
1052 match idx {
1053 0 => BiomeType::Ocean,
1054 1 => BiomeType::DeepOcean,
1055 2 => BiomeType::Beach,
1056 3 => BiomeType::Desert,
1057 4 => BiomeType::Savanna,
1058 5 => BiomeType::Grassland,
1059 6 => BiomeType::Shrubland,
1060 7 => BiomeType::TemperateForest,
1061 8 => BiomeType::TropicalForest,
1062 9 => BiomeType::Boreal,
1063 10 => BiomeType::Taiga,
1064 11 => BiomeType::Tundra,
1065 12 => BiomeType::Arctic,
1066 13 => BiomeType::Mountain,
1067 14 => BiomeType::AlpineGlacier,
1068 15 => BiomeType::Swamp,
1069 16 => BiomeType::Mangrove,
1070 17 => BiomeType::Volcanic,
1071 18 => BiomeType::Badlands,
1072 _ => BiomeType::Mushroom,
1073 }
1074}
1075
1076#[derive(Clone, Debug, Default)]
1080pub struct BiomeAdjacency {
1081 pub adjacency: [[usize; 20]; 20],
1083}
1084
1085impl BiomeAdjacency {
1086 pub fn from_map(bm: &BiomeMap) -> Self {
1087 let mut adj = Self::default();
1088 let dirs: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
1089 for y in 0..bm.height {
1090 for x in 0..bm.width {
1091 let b0 = bm.get(x, y) as usize;
1092 for (dx, dy) in &dirs {
1093 let nx = x as i32 + dx;
1094 let ny = y as i32 + dy;
1095 if nx >= 0 && nx < bm.width as i32 && ny >= 0 && ny < bm.height as i32 {
1096 let b1 = bm.get(nx as usize, ny as usize) as usize;
1097 if b0 != b1 && b0 < 20 && b1 < 20 {
1098 adj.adjacency[b0][b1] += 1;
1099 }
1100 }
1101 }
1102 }
1103 }
1104 adj
1105 }
1106
1107 pub fn boundary_length(&self, biome: BiomeType) -> usize {
1109 self.adjacency[biome as usize].iter().sum()
1110 }
1111
1112 pub fn neighbors(&self, biome: BiomeType) -> Vec<BiomeType> {
1114 self.adjacency[biome as usize].iter()
1115 .enumerate()
1116 .filter(|(_, &c)| c > 0)
1117 .map(|(i, _)| biome_from_index(i))
1118 .collect()
1119 }
1120}
1121
1122pub struct BiomeNoiseVariator {
1126 temperature_noise_scale: f32,
1127 humidity_noise_scale: f32,
1128 seed: u64,
1129}
1130
1131impl BiomeNoiseVariator {
1132 pub fn new(temperature_scale: f32, humidity_scale: f32, seed: u64) -> Self {
1133 Self {
1134 temperature_noise_scale: temperature_scale,
1135 humidity_noise_scale: humidity_scale,
1136 seed,
1137 }
1138 }
1139
1140 pub fn vary(&self, params: &BiomeParams, x: f32, y: f32) -> BiomeParams {
1142 let noise = crate::terrain::heightmap::GradientNoisePublic::new(self.seed);
1143 let tn = noise.noise2d(x * 0.05, y * 0.05) * 2.0 - 1.0;
1144 let hn = noise.noise2d(x * 0.05 + 100.0, y * 0.05 + 100.0) * 2.0 - 1.0;
1145 BiomeParams {
1146 temperature: (params.temperature + tn * self.temperature_noise_scale).clamp(0.0, 1.0),
1147 humidity: (params.humidity + hn * self.humidity_noise_scale).clamp(0.0, 1.0),
1148 ..*params
1149 }
1150 }
1151}
1152
1153pub struct RiverSimulator;
1157
1158impl RiverSimulator {
1159 pub fn generate(heightmap: &crate::terrain::heightmap::HeightMap, threshold: f32) -> crate::terrain::heightmap::HeightMap {
1162 let w = heightmap.width;
1163 let h = heightmap.height;
1164 let flow_dirs = heightmap.flow_map();
1165 let mut accumulation = vec![1.0f32; w * h];
1167 let mut order: Vec<(usize, usize)> = (0..h).flat_map(|y| (0..w).map(move |x| (x, y))).collect();
1169 order.sort_by(|&(ax, ay), &(bx, by)| {
1170 heightmap.get(bx, by).partial_cmp(&heightmap.get(ax, ay))
1171 .unwrap_or(std::cmp::Ordering::Equal)
1172 });
1173 let dirs: [(f32, f32); 8] = [
1174 (-1.0,-1.0),(0.0,-1.0),(1.0,-1.0),
1175 (-1.0, 0.0), (1.0, 0.0),
1176 (-1.0, 1.0),(0.0, 1.0),(1.0, 1.0),
1177 ];
1178 for (x, y) in &order {
1179 let dir_idx = (flow_dirs.get(*x, *y) * 8.0) as usize;
1180 if dir_idx >= 8 { continue; }
1181 let (dx, dy) = dirs[dir_idx];
1182 let nx = (*x as i32 + dx as i32) as usize;
1183 let ny = (*y as i32 + dy as i32) as usize;
1184 if nx < w && ny < h {
1185 let val = accumulation[y * w + x];
1186 accumulation[ny * w + nx] += val;
1187 }
1188 }
1189 let max_acc = accumulation.iter().cloned().fold(0.0f32, f32::max);
1191 let mut out = crate::terrain::heightmap::HeightMap::new(w, h);
1192 if max_acc > 0.0 {
1193 for i in 0..(w*h) {
1194 let norm = accumulation[i] / max_acc;
1195 out.data[i] = if norm > threshold { 1.0 } else { 0.0 };
1196 }
1197 }
1198 out
1199 }
1200}
1201
1202#[derive(Clone, Debug)]
1206pub struct BiomeTransitionMap {
1207 pub width: usize,
1208 pub height: usize,
1209 pub transitions: Vec<f32>,
1211}
1212
1213impl BiomeTransitionMap {
1214 pub fn from_map(bm: &BiomeMap, radius: usize) -> Self {
1216 let w = bm.width;
1217 let h = bm.height;
1218 let mut transitions = vec![0.0f32; w * h];
1219 for y in 0..h {
1220 for x in 0..w {
1221 let base = bm.get(x, y);
1222 let mut diff_count = 0usize;
1223 let mut total = 0usize;
1224 for dy in -(radius as i32)..=(radius as i32) {
1225 for dx in -(radius as i32)..=(radius as i32) {
1226 let nx = x as i32 + dx;
1227 let ny = y as i32 + dy;
1228 if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1229 total += 1;
1230 if bm.get(nx as usize, ny as usize) != base {
1231 diff_count += 1;
1232 }
1233 }
1234 }
1235 }
1236 transitions[y * w + x] = if total > 0 { diff_count as f32 / total as f32 } else { 0.0 };
1237 }
1238 }
1239 Self { width: w, height: h, transitions }
1240 }
1241
1242 pub fn get(&self, x: usize, y: usize) -> f32 {
1244 if x < self.width && y < self.height {
1245 self.transitions[y * self.width + x]
1246 } else {
1247 0.0
1248 }
1249 }
1250}
1251
1252#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1256pub enum ClimateZone {
1257 Tropical,
1259 Arid,
1261 Temperate,
1263 Continental,
1265 Polar,
1267}
1268
1269impl ClimateZone {
1270 pub fn from_params(temp: f32, humidity: f32, altitude: f32) -> Self {
1271 if altitude > 0.85 { return Self::Polar; }
1272 if temp < 0.15 { return Self::Polar; }
1273 if temp > 0.7 && humidity < 0.2 { return Self::Arid; }
1274 if temp > 0.6 { return Self::Tropical; }
1275 if temp > 0.35 && humidity > 0.3 { return Self::Temperate; }
1276 if temp > 0.25 { return Self::Continental; }
1277 Self::Polar
1278 }
1279
1280 pub fn name(self) -> &'static str {
1281 match self {
1282 Self::Tropical => "Tropical",
1283 Self::Arid => "Arid",
1284 Self::Temperate => "Temperate",
1285 Self::Continental => "Continental",
1286 Self::Polar => "Polar",
1287 }
1288 }
1289}
1290
1291#[derive(Clone, Debug)]
1295pub struct PrecipitationPattern {
1296 pub monthly: [f32; 12],
1298 pub annual: f32,
1300 pub peak_month: usize,
1302 pub mostly_snow: bool,
1304}
1305
1306impl PrecipitationPattern {
1307 pub fn for_biome(biome: BiomeType) -> Self {
1308 let monthly: [f32; 12] = match biome {
1309 BiomeType::TropicalForest => [250.0, 230.0, 240.0, 280.0, 300.0, 350.0, 380.0, 370.0, 320.0, 290.0, 260.0, 240.0],
1310 BiomeType::Desert => [5.0, 3.0, 4.0, 8.0, 10.0, 2.0, 1.0, 1.0, 3.0, 6.0, 5.0, 4.0],
1311 BiomeType::Grassland => [30.0, 35.0, 45.0, 60.0, 80.0, 90.0, 85.0, 75.0, 55.0, 45.0, 35.0, 28.0],
1312 BiomeType::TemperateForest=> [80.0, 75.0, 85.0, 90.0, 95.0, 100.0, 90.0, 85.0, 90.0, 95.0, 90.0, 85.0],
1313 BiomeType::Savanna => [10.0, 15.0, 30.0, 60.0, 100.0, 120.0, 130.0, 120.0, 100.0, 60.0, 25.0, 12.0],
1314 BiomeType::Tundra => [15.0, 12.0, 14.0, 18.0, 22.0, 30.0, 35.0, 33.0, 25.0, 20.0, 17.0, 14.0],
1315 BiomeType::Arctic => [5.0, 4.0, 5.0, 6.0, 8.0, 12.0, 15.0, 14.0, 10.0, 7.0, 6.0, 5.0],
1316 _ => [50.0; 12],
1317 };
1318 let annual: f32 = monthly.iter().sum();
1319 let peak_month = monthly.iter().enumerate()
1320 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1321 .map(|(i, _)| i).unwrap_or(0);
1322 let mostly_snow = matches!(biome, BiomeType::Arctic | BiomeType::AlpineGlacier | BiomeType::Tundra);
1323 Self { monthly, annual, peak_month, mostly_snow }
1324 }
1325
1326 pub fn monthly_mm(&self, month: usize) -> f32 {
1327 self.monthly[month % 12]
1328 }
1329
1330 pub fn is_dry_season(&self, month: usize) -> bool {
1331 let m = month % 12;
1332 self.monthly[m] < self.annual / 12.0 * 0.5
1333 }
1334}
1335
1336#[derive(Clone, Debug)]
1340pub struct TemperatureRange {
1341 pub monthly_avg: [f32; 12],
1343 pub annual_mean: f32,
1344 pub annual_min: f32,
1345 pub annual_max: f32,
1346}
1347
1348impl TemperatureRange {
1349 pub fn for_biome(biome: BiomeType) -> Self {
1350 let monthly_avg: [f32; 12] = match biome {
1351 BiomeType::TropicalForest => [27.0, 27.5, 28.0, 28.0, 27.5, 27.0, 26.5, 26.5, 27.0, 27.0, 27.0, 27.0],
1352 BiomeType::Desert => [15.0, 18.0, 23.0, 28.0, 33.0, 38.0, 40.0, 39.0, 35.0, 28.0, 21.0, 16.0],
1353 BiomeType::Grassland => [2.0, 4.0, 9.0, 14.0, 19.0, 23.0, 25.0, 24.0, 20.0, 14.0, 7.0, 3.0],
1354 BiomeType::TemperateForest=> [3.0, 5.0, 9.0, 14.0, 18.0, 21.0, 23.0, 22.0, 18.0, 13.0, 7.0, 4.0],
1355 BiomeType::Tundra => [-20.0,-18.0,-12.0,-3.0, 3.0, 8.0, 11.0, 10.0, 5.0, -2.0,-10.0,-17.0],
1356 BiomeType::Arctic => [-35.0,-33.0,-28.0,-15.0,-5.0, 1.0, 3.0, 2.0, -3.0,-14.0,-25.0,-32.0],
1357 BiomeType::AlpineGlacier => [-15.0,-14.0,-10.0,-4.0, 0.0, 3.0, 5.0, 4.0, 1.0, -4.0,-10.0,-14.0],
1358 _ => [10.0; 12],
1359 };
1360 let annual_mean: f32 = monthly_avg.iter().sum::<f32>() / 12.0;
1361 let annual_min: f32 = monthly_avg.iter().cloned().fold(f32::INFINITY, f32::min);
1362 let annual_max: f32 = monthly_avg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1363 Self { monthly_avg, annual_mean, annual_min, annual_max }
1364 }
1365
1366 pub fn is_frozen(&self, month: usize) -> bool {
1367 self.monthly_avg[month % 12] < 0.0
1368 }
1369
1370 pub fn frost_free_months(&self) -> usize {
1371 self.monthly_avg.iter().filter(|&&t| t > 0.0).count()
1372 }
1373}
1374
1375pub struct BiomeSuccession;
1379
1380impl BiomeSuccession {
1381 pub fn successor(biome: BiomeType, years: f32) -> BiomeType {
1383 match biome {
1384 BiomeType::Badlands if years > 50.0 => BiomeType::Shrubland,
1385 BiomeType::Shrubland if years > 100.0 => BiomeType::Grassland,
1386 BiomeType::Grassland if years > 200.0 => BiomeType::TemperateForest,
1387 BiomeType::Tundra if years > 500.0 => BiomeType::Taiga,
1388 BiomeType::Taiga if years > 1000.0 => BiomeType::Boreal,
1389 BiomeType::Desert if years > 100.0 => BiomeType::Shrubland,
1390 BiomeType::Volcanic if years > 20.0 => BiomeType::Badlands,
1391 BiomeType::Beach if years > 30.0 => BiomeType::Grassland,
1392 _ => biome,
1393 }
1394 }
1395
1396 pub fn time_to_next(biome: BiomeType) -> f32 {
1398 match biome {
1399 BiomeType::Volcanic => 20.0,
1400 BiomeType::Beach => 30.0,
1401 BiomeType::Badlands => 50.0,
1402 BiomeType::Shrubland => 100.0,
1403 BiomeType::Desert => 100.0,
1404 BiomeType::Grassland => 200.0,
1405 BiomeType::Tundra => 500.0,
1406 BiomeType::Taiga => 1000.0,
1407 _ => f32::INFINITY,
1408 }
1409 }
1410}
1411
1412#[cfg(test)]
1415mod extended_biome_tests {
1416 use super::*;
1417 use crate::terrain::heightmap::FractalNoise;
1418
1419 #[test]
1420 fn test_biome_stats_from_map() {
1421 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1422 let sim = ClimateSimulator::default();
1423 let climate = sim.simulate(&hm);
1424 let bm = BiomeMap::from_heightmap(&hm, &climate);
1425 let stats = BiomeStats::from_map(&bm);
1426 assert_eq!(stats.total, 32 * 32);
1427 let total: usize = stats.counts.iter().sum();
1428 assert_eq!(total, 32 * 32);
1429 }
1430
1431 #[test]
1432 fn test_biome_stats_diversity() {
1433 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1434 let sim = ClimateSimulator::default();
1435 let climate = sim.simulate(&hm);
1436 let bm = BiomeMap::from_heightmap(&hm, &climate);
1437 let stats = BiomeStats::from_map(&bm);
1438 let div = stats.diversity_index();
1439 assert!(div >= 0.0 && div <= 1.0);
1440 }
1441
1442 #[test]
1443 fn test_biome_adjacency() {
1444 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1445 let sim = ClimateSimulator::default();
1446 let climate = sim.simulate(&hm);
1447 let bm = BiomeMap::from_heightmap(&hm, &climate);
1448 let adj = BiomeAdjacency::from_map(&bm);
1449 for i in 0..20 {
1451 for j in 0..20 {
1452 assert_eq!(adj.adjacency[i][j], adj.adjacency[j][i],
1453 "Adjacency should be symmetric");
1454 }
1455 }
1456 }
1457
1458 #[test]
1459 fn test_precipitation_pattern() {
1460 let pat = PrecipitationPattern::for_biome(BiomeType::TropicalForest);
1461 assert!(pat.annual > 1000.0, "Tropical forest should be wet");
1462 let dry = PrecipitationPattern::for_biome(BiomeType::Desert);
1463 assert!(dry.annual < 100.0, "Desert should be dry");
1464 }
1465
1466 #[test]
1467 fn test_temperature_range() {
1468 let tr = TemperatureRange::for_biome(BiomeType::Arctic);
1469 assert!(tr.annual_max < 10.0, "Arctic should be cold year-round");
1470 let tropic = TemperatureRange::for_biome(BiomeType::TropicalForest);
1471 assert!(tropic.annual_min > 20.0, "Tropical should be warm year-round");
1472 }
1473
1474 #[test]
1475 fn test_climate_zone_classification() {
1476 assert_eq!(ClimateZone::from_params(0.9, 0.9, 0.3), ClimateZone::Tropical);
1477 assert_eq!(ClimateZone::from_params(0.8, 0.1, 0.3), ClimateZone::Arid);
1478 assert_eq!(ClimateZone::from_params(0.5, 0.6, 0.3), ClimateZone::Temperate);
1479 assert_eq!(ClimateZone::from_params(0.1, 0.3, 0.3), ClimateZone::Polar);
1480 }
1481
1482 #[test]
1483 fn test_biome_succession() {
1484 assert_eq!(BiomeSuccession::successor(BiomeType::Volcanic, 25.0), BiomeType::Badlands);
1485 assert_eq!(BiomeSuccession::successor(BiomeType::Volcanic, 5.0), BiomeType::Volcanic);
1486 assert_eq!(BiomeSuccession::successor(BiomeType::Badlands, 100.0), BiomeType::Shrubland);
1487 }
1488
1489 #[test]
1490 fn test_biome_transition_map() {
1491 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1492 let sim = ClimateSimulator::default();
1493 let climate = sim.simulate(&hm);
1494 let bm = BiomeMap::from_heightmap(&hm, &climate);
1495 let tm = BiomeTransitionMap::from_map(&bm, 2);
1496 assert_eq!(tm.transitions.len(), 32 * 32);
1497 assert!(tm.transitions.iter().all(|&v| v >= 0.0 && v <= 1.0));
1498 }
1499
1500 #[test]
1501 fn test_biome_from_index_coverage() {
1502 for i in 0..20 {
1503 let b = biome_from_index(i);
1504 assert_eq!(b as usize, i);
1505 }
1506 }
1507
1508 #[test]
1509 fn test_river_simulator() {
1510 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1511 let rivers = RiverSimulator::generate(&hm, 0.9);
1512 assert_eq!(rivers.data.len(), 32 * 32);
1513 }
1514}