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;
765 0.5 + x * (1.0 - x.abs() * 0.5)
766 }
767 }
768}
769
770#[derive(Clone, Copy, Debug)]
774pub struct SeasonFactor {
775 pub vegetation_green: f32,
777 pub autumn_shift: f32,
779 pub snow_cover: f32,
781 pub density_scale: f32,
783}
784
785impl SeasonFactor {
786 pub fn season_factor(biome: BiomeType, month: u32) -> Self {
788 let month = (month % 12) as f32;
789 let summer_t = ((month - 6.0) * std::f32::consts::PI / 6.0).cos() * 0.5 + 0.5;
791 let winter_t = 1.0 - summer_t;
793
794 match biome {
795 BiomeType::TemperateForest | BiomeType::Boreal => Self {
796 vegetation_green: 0.2 + summer_t * 0.8,
797 autumn_shift: if month > 7.0 && month < 11.0 { (month - 7.0) * 0.25 } else { 0.0 },
798 snow_cover: (winter_t - 0.6).max(0.0) * 2.5,
799 density_scale: 0.3 + summer_t * 0.7,
800 },
801 BiomeType::Taiga | BiomeType::Tundra => Self {
802 vegetation_green: 0.1 + summer_t * 0.7,
803 autumn_shift: 0.0,
804 snow_cover: winter_t * 0.9,
805 density_scale: 0.1 + summer_t * 0.6,
806 },
807 BiomeType::Arctic | BiomeType::AlpineGlacier => Self {
808 vegetation_green: summer_t * 0.2,
809 autumn_shift: 0.0,
810 snow_cover: 0.5 + winter_t * 0.5,
811 density_scale: summer_t * 0.15,
812 },
813 BiomeType::Grassland | BiomeType::Savanna => Self {
814 vegetation_green: 0.4 + summer_t * 0.5,
815 autumn_shift: (winter_t - 0.3).max(0.0) * 0.5,
816 snow_cover: (winter_t - 0.8).max(0.0) * 2.0,
817 density_scale: 0.5 + summer_t * 0.5,
818 },
819 BiomeType::Desert | BiomeType::Badlands => Self {
820 vegetation_green: 0.1,
821 autumn_shift: 0.0,
822 snow_cover: 0.0,
823 density_scale: 0.8 + summer_t * 0.2,
824 },
825 BiomeType::TropicalForest | BiomeType::Mangrove | BiomeType::Swamp => Self {
827 vegetation_green: 0.9,
828 autumn_shift: 0.0,
829 snow_cover: 0.0,
830 density_scale: 1.0,
831 },
832 _ => Self {
833 vegetation_green: 0.5 + summer_t * 0.5,
834 autumn_shift: 0.0,
835 snow_cover: winter_t * 0.3,
836 density_scale: 0.6 + summer_t * 0.4,
837 },
838 }
839 }
840}
841
842#[cfg(test)]
845mod tests {
846 use super::*;
847 use crate::terrain::heightmap::FractalNoise;
848
849 #[test]
850 fn test_biome_type_names() {
851 assert_eq!(BiomeType::Desert.name(), "Desert");
852 assert_eq!(BiomeType::TropicalForest.name(), "Tropical Forest");
853 assert_eq!(BiomeType::AlpineGlacier.name(), "Alpine Glacier");
854 }
855
856 #[test]
857 fn test_biome_type_properties() {
858 assert!(BiomeType::Ocean.is_aquatic());
859 assert!(!BiomeType::Desert.is_aquatic());
860 assert!(BiomeType::Arctic.is_cold());
861 assert!(!BiomeType::Desert.is_cold());
862 assert!(BiomeType::TropicalForest.has_trees());
863 assert!(!BiomeType::Arctic.has_trees());
864 }
865
866 #[test]
867 fn test_biome_classifier_desert() {
868 let p = BiomeParams {
869 temperature: 0.8, humidity: 0.1, altitude: 0.3, slope: 0.05,
870 coast_distance: 0.9, volcanic: false,
871 };
872 assert_eq!(BiomeClassifier::classify(&p), BiomeType::Desert);
873 }
874
875 #[test]
876 fn test_biome_classifier_ocean() {
877 let p = BiomeParams {
878 temperature: 0.5, humidity: 0.8, altitude: 0.01, slope: 0.0,
879 coast_distance: 0.0, volcanic: false,
880 };
881 assert!(matches!(
882 BiomeClassifier::classify(&p),
883 BiomeType::Ocean | BiomeType::DeepOcean
884 ));
885 }
886
887 #[test]
888 fn test_biome_classifier_alpine() {
889 let p = BiomeParams {
890 temperature: 0.2, humidity: 0.3, altitude: 0.96, slope: 0.3,
891 coast_distance: 0.8, volcanic: false,
892 };
893 assert_eq!(BiomeClassifier::classify(&p), BiomeType::AlpineGlacier);
894 }
895
896 #[test]
897 fn test_biome_classifier_tropical() {
898 let p = BiomeParams {
899 temperature: 0.9, humidity: 0.9, altitude: 0.4, slope: 0.05,
900 coast_distance: 0.5, volcanic: false,
901 };
902 assert_eq!(BiomeClassifier::classify(&p), BiomeType::TropicalForest);
903 }
904
905 #[test]
906 fn test_biome_classifier_volcanic() {
907 let p = BiomeParams {
908 temperature: 0.7, humidity: 0.2, altitude: 0.8, slope: 0.75,
909 coast_distance: 0.7, volcanic: true,
910 };
911 assert_eq!(BiomeClassifier::classify(&p), BiomeType::Volcanic);
912 }
913
914 #[test]
915 fn test_climate_simulator() {
916 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
917 let sim = ClimateSimulator::default();
918 let climate = sim.simulate(&hm);
919 assert_eq!(climate.temperature.data.len(), 32 * 32);
920 assert_eq!(climate.humidity.data.len(), 32 * 32);
921 assert!(climate.temperature.min_value() >= 0.0);
922 assert!(climate.temperature.max_value() <= 1.0);
923 }
924
925 #[test]
926 fn test_biome_map_from_heightmap() {
927 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
928 let sim = ClimateSimulator::default();
929 let climate = sim.simulate(&hm);
930 let bm = BiomeMap::from_heightmap(&hm, &climate);
931 assert_eq!(bm.biomes.len(), 32 * 32);
932 }
933
934 #[test]
935 fn test_biome_map_blend_weights() {
936 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
937 let sim = ClimateSimulator::default();
938 let climate = sim.simulate(&hm);
939 let bm = BiomeMap::from_heightmap(&hm, &climate);
940 let weights = bm.blend_weights(16.5, 16.5);
941 let total: f32 = weights.iter().map(|(_, w)| w).sum();
942 assert!((total - 1.0).abs() < 1e-4);
943 }
944
945 #[test]
946 fn test_vegetation_density() {
947 let d = VegetationDensity::for_biome(BiomeType::TropicalForest);
948 assert!(d.tree_density > 0.5);
949 let d2 = VegetationDensity::for_biome(BiomeType::Arctic);
950 assert!(d2.tree_density < 0.1);
951 }
952
953 #[test]
954 fn test_biome_colors_all_defined() {
955 let all = [
956 BiomeType::Ocean, BiomeType::DeepOcean, BiomeType::Beach,
957 BiomeType::Desert, BiomeType::Savanna, BiomeType::Grassland,
958 BiomeType::Shrubland, BiomeType::TemperateForest, BiomeType::TropicalForest,
959 BiomeType::Boreal, BiomeType::Taiga, BiomeType::Tundra,
960 BiomeType::Arctic, BiomeType::Mountain, BiomeType::AlpineGlacier,
961 BiomeType::Swamp, BiomeType::Mangrove, BiomeType::Volcanic,
962 BiomeType::Badlands, BiomeType::Mushroom,
963 ];
964 for biome in all {
965 let color = BiomeColor::for_biome(biome);
966 assert!(color.ground.x >= 0.0 && color.ground.x <= 1.0);
968 }
969 }
970
971 #[test]
972 fn test_season_factor() {
973 let summer = SeasonFactor::season_factor(BiomeType::TemperateForest, 6);
974 let winter = SeasonFactor::season_factor(BiomeType::TemperateForest, 0);
975 assert!(summer.vegetation_green > winter.vegetation_green);
976 assert!(winter.snow_cover >= summer.snow_cover);
977 }
978
979 #[test]
980 fn test_transition_zone() {
981 let sharp = TransitionZone::new(BiomeType::Grassland, BiomeType::Desert, 10.0);
984 assert_eq!(sharp.blend_factor(0.25), 0.0);
985 assert_eq!(sharp.blend_factor(0.75), 1.0);
986 let tz = TransitionZone::new(BiomeType::Grassland, BiomeType::TemperateForest, 10.0);
987 assert!((tz.blend_factor(0.0) - 0.0).abs() < 0.01);
988 assert!((tz.blend_factor(1.0) - 1.0).abs() < 0.01);
989 let mid = tz.blend_factor(0.5);
990 assert!(mid > 0.0 && mid < 1.0);
991 assert!(tz.blend_factor(0.3) < tz.blend_factor(0.7));
992 }
993}
994
995#[derive(Clone, Debug, Default)]
999pub struct BiomeStats {
1000 pub counts: [usize; 20],
1002 pub total: usize,
1004}
1005
1006impl BiomeStats {
1007 pub fn from_map(bm: &BiomeMap) -> Self {
1009 let mut stats = Self::default();
1010 stats.total = bm.biomes.len();
1011 for &b in &bm.biomes {
1012 let idx = b as usize;
1013 if idx < 20 { stats.counts[idx] += 1; }
1014 }
1015 stats
1016 }
1017
1018 pub fn fraction(&self, biome: BiomeType) -> f32 {
1020 if self.total == 0 { return 0.0; }
1021 self.counts[biome as usize] as f32 / self.total as f32
1022 }
1023
1024 pub fn dominant_biome(&self) -> BiomeType {
1026 let idx = self.counts.iter().enumerate()
1027 .max_by_key(|(_, &c)| c)
1028 .map(|(i, _)| i)
1029 .unwrap_or(0);
1030 biome_from_index(idx)
1031 }
1032
1033 pub fn sorted_biomes(&self) -> Vec<(BiomeType, usize)> {
1035 let mut pairs: Vec<(BiomeType, usize)> = self.counts.iter()
1036 .enumerate()
1037 .filter(|(_, &c)| c > 0)
1038 .map(|(i, &c)| (biome_from_index(i), c))
1039 .collect();
1040 pairs.sort_by(|a, b| b.1.cmp(&a.1));
1041 pairs
1042 }
1043
1044 pub fn diversity_index(&self) -> f32 {
1046 if self.total == 0 { return 0.0; }
1047 let n = self.total as f32;
1048 let entropy: f32 = self.counts.iter()
1049 .filter(|&&c| c > 0)
1050 .map(|&c| {
1051 let p = c as f32 / n;
1052 -p * p.ln()
1053 })
1054 .sum();
1055 entropy / (20.0f32).ln()
1057 }
1058}
1059
1060pub fn biome_from_index(idx: usize) -> BiomeType {
1061 match idx {
1062 0 => BiomeType::Ocean,
1063 1 => BiomeType::DeepOcean,
1064 2 => BiomeType::Beach,
1065 3 => BiomeType::Desert,
1066 4 => BiomeType::Savanna,
1067 5 => BiomeType::Grassland,
1068 6 => BiomeType::Shrubland,
1069 7 => BiomeType::TemperateForest,
1070 8 => BiomeType::TropicalForest,
1071 9 => BiomeType::Boreal,
1072 10 => BiomeType::Taiga,
1073 11 => BiomeType::Tundra,
1074 12 => BiomeType::Arctic,
1075 13 => BiomeType::Mountain,
1076 14 => BiomeType::AlpineGlacier,
1077 15 => BiomeType::Swamp,
1078 16 => BiomeType::Mangrove,
1079 17 => BiomeType::Volcanic,
1080 18 => BiomeType::Badlands,
1081 _ => BiomeType::Mushroom,
1082 }
1083}
1084
1085#[derive(Clone, Debug, Default)]
1089pub struct BiomeAdjacency {
1090 pub adjacency: [[usize; 20]; 20],
1092}
1093
1094impl BiomeAdjacency {
1095 pub fn from_map(bm: &BiomeMap) -> Self {
1096 let mut adj = Self::default();
1097 let dirs: [(i32, i32); 4] = [(1,0),(-1,0),(0,1),(0,-1)];
1098 for y in 0..bm.height {
1099 for x in 0..bm.width {
1100 let b0 = bm.get(x, y) as usize;
1101 for (dx, dy) in &dirs {
1102 let nx = x as i32 + dx;
1103 let ny = y as i32 + dy;
1104 if nx >= 0 && nx < bm.width as i32 && ny >= 0 && ny < bm.height as i32 {
1105 let b1 = bm.get(nx as usize, ny as usize) as usize;
1106 if b0 != b1 && b0 < 20 && b1 < 20 {
1107 adj.adjacency[b0][b1] += 1;
1108 }
1109 }
1110 }
1111 }
1112 }
1113 adj
1114 }
1115
1116 pub fn boundary_length(&self, biome: BiomeType) -> usize {
1118 self.adjacency[biome as usize].iter().sum()
1119 }
1120
1121 pub fn neighbors(&self, biome: BiomeType) -> Vec<BiomeType> {
1123 self.adjacency[biome as usize].iter()
1124 .enumerate()
1125 .filter(|(_, &c)| c > 0)
1126 .map(|(i, _)| biome_from_index(i))
1127 .collect()
1128 }
1129}
1130
1131pub struct BiomeNoiseVariator {
1135 temperature_noise_scale: f32,
1136 humidity_noise_scale: f32,
1137 seed: u64,
1138}
1139
1140impl BiomeNoiseVariator {
1141 pub fn new(temperature_scale: f32, humidity_scale: f32, seed: u64) -> Self {
1142 Self {
1143 temperature_noise_scale: temperature_scale,
1144 humidity_noise_scale: humidity_scale,
1145 seed,
1146 }
1147 }
1148
1149 pub fn vary(&self, params: &BiomeParams, x: f32, y: f32) -> BiomeParams {
1151 let noise = crate::terrain::heightmap::GradientNoisePublic::new(self.seed);
1152 let tn = noise.noise2d(x * 0.05, y * 0.05) * 2.0 - 1.0;
1153 let hn = noise.noise2d(x * 0.05 + 100.0, y * 0.05 + 100.0) * 2.0 - 1.0;
1154 BiomeParams {
1155 temperature: (params.temperature + tn * self.temperature_noise_scale).clamp(0.0, 1.0),
1156 humidity: (params.humidity + hn * self.humidity_noise_scale).clamp(0.0, 1.0),
1157 ..*params
1158 }
1159 }
1160}
1161
1162pub struct RiverSimulator;
1166
1167impl RiverSimulator {
1168 pub fn generate(heightmap: &crate::terrain::heightmap::HeightMap, threshold: f32) -> crate::terrain::heightmap::HeightMap {
1171 let w = heightmap.width;
1172 let h = heightmap.height;
1173 let flow_dirs = heightmap.flow_map();
1174 let mut accumulation = vec![1.0f32; w * h];
1176 let mut order: Vec<(usize, usize)> = (0..h).flat_map(|y| (0..w).map(move |x| (x, y))).collect();
1178 order.sort_by(|&(ax, ay), &(bx, by)| {
1179 heightmap.get(bx, by).partial_cmp(&heightmap.get(ax, ay))
1180 .unwrap_or(std::cmp::Ordering::Equal)
1181 });
1182 let dirs: [(f32, f32); 8] = [
1183 (-1.0,-1.0),(0.0,-1.0),(1.0,-1.0),
1184 (-1.0, 0.0), (1.0, 0.0),
1185 (-1.0, 1.0),(0.0, 1.0),(1.0, 1.0),
1186 ];
1187 for (x, y) in &order {
1188 let dir_idx = (flow_dirs.get(*x, *y) * 8.0) as usize;
1189 if dir_idx >= 8 { continue; }
1190 let (dx, dy) = dirs[dir_idx];
1191 let nx = (*x as i32 + dx as i32) as usize;
1192 let ny = (*y as i32 + dy as i32) as usize;
1193 if nx < w && ny < h {
1194 let val = accumulation[y * w + x];
1195 accumulation[ny * w + nx] += val;
1196 }
1197 }
1198 let max_acc = accumulation.iter().cloned().fold(0.0f32, f32::max);
1200 let mut out = crate::terrain::heightmap::HeightMap::new(w, h);
1201 if max_acc > 0.0 {
1202 for i in 0..(w*h) {
1203 let norm = accumulation[i] / max_acc;
1204 out.data[i] = if norm > threshold { 1.0 } else { 0.0 };
1205 }
1206 }
1207 out
1208 }
1209}
1210
1211#[derive(Clone, Debug)]
1215pub struct BiomeTransitionMap {
1216 pub width: usize,
1217 pub height: usize,
1218 pub transitions: Vec<f32>,
1220}
1221
1222impl BiomeTransitionMap {
1223 pub fn from_map(bm: &BiomeMap, radius: usize) -> Self {
1225 let w = bm.width;
1226 let h = bm.height;
1227 let mut transitions = vec![0.0f32; w * h];
1228 for y in 0..h {
1229 for x in 0..w {
1230 let base = bm.get(x, y);
1231 let mut diff_count = 0usize;
1232 let mut total = 0usize;
1233 for dy in -(radius as i32)..=(radius as i32) {
1234 for dx in -(radius as i32)..=(radius as i32) {
1235 let nx = x as i32 + dx;
1236 let ny = y as i32 + dy;
1237 if nx >= 0 && nx < w as i32 && ny >= 0 && ny < h as i32 {
1238 total += 1;
1239 if bm.get(nx as usize, ny as usize) != base {
1240 diff_count += 1;
1241 }
1242 }
1243 }
1244 }
1245 transitions[y * w + x] = if total > 0 { diff_count as f32 / total as f32 } else { 0.0 };
1246 }
1247 }
1248 Self { width: w, height: h, transitions }
1249 }
1250
1251 pub fn get(&self, x: usize, y: usize) -> f32 {
1253 if x < self.width && y < self.height {
1254 self.transitions[y * self.width + x]
1255 } else {
1256 0.0
1257 }
1258 }
1259}
1260
1261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1265pub enum ClimateZone {
1266 Tropical,
1268 Arid,
1270 Temperate,
1272 Continental,
1274 Polar,
1276}
1277
1278impl ClimateZone {
1279 pub fn from_params(temp: f32, humidity: f32, altitude: f32) -> Self {
1280 if altitude > 0.85 { return Self::Polar; }
1281 if temp < 0.15 { return Self::Polar; }
1282 if temp > 0.7 && humidity < 0.2 { return Self::Arid; }
1283 if temp > 0.6 { return Self::Tropical; }
1284 if temp > 0.35 && humidity > 0.3 { return Self::Temperate; }
1285 if temp > 0.25 { return Self::Continental; }
1286 Self::Polar
1287 }
1288
1289 pub fn name(self) -> &'static str {
1290 match self {
1291 Self::Tropical => "Tropical",
1292 Self::Arid => "Arid",
1293 Self::Temperate => "Temperate",
1294 Self::Continental => "Continental",
1295 Self::Polar => "Polar",
1296 }
1297 }
1298}
1299
1300#[derive(Clone, Debug)]
1304pub struct PrecipitationPattern {
1305 pub monthly: [f32; 12],
1307 pub annual: f32,
1309 pub peak_month: usize,
1311 pub mostly_snow: bool,
1313}
1314
1315impl PrecipitationPattern {
1316 pub fn for_biome(biome: BiomeType) -> Self {
1317 let monthly: [f32; 12] = match biome {
1318 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],
1319 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],
1320 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],
1321 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],
1322 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],
1323 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],
1324 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],
1325 _ => [50.0; 12],
1326 };
1327 let annual: f32 = monthly.iter().sum();
1328 let peak_month = monthly.iter().enumerate()
1329 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1330 .map(|(i, _)| i).unwrap_or(0);
1331 let mostly_snow = matches!(biome, BiomeType::Arctic | BiomeType::AlpineGlacier | BiomeType::Tundra);
1332 Self { monthly, annual, peak_month, mostly_snow }
1333 }
1334
1335 pub fn monthly_mm(&self, month: usize) -> f32 {
1336 self.monthly[month % 12]
1337 }
1338
1339 pub fn is_dry_season(&self, month: usize) -> bool {
1340 let m = month % 12;
1341 self.monthly[m] < self.annual / 12.0 * 0.5
1342 }
1343}
1344
1345#[derive(Clone, Debug)]
1349pub struct TemperatureRange {
1350 pub monthly_avg: [f32; 12],
1352 pub annual_mean: f32,
1353 pub annual_min: f32,
1354 pub annual_max: f32,
1355}
1356
1357impl TemperatureRange {
1358 pub fn for_biome(biome: BiomeType) -> Self {
1359 let monthly_avg: [f32; 12] = match biome {
1360 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],
1361 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],
1362 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],
1363 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],
1364 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],
1365 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],
1366 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],
1367 _ => [10.0; 12],
1368 };
1369 let annual_mean: f32 = monthly_avg.iter().sum::<f32>() / 12.0;
1370 let annual_min: f32 = monthly_avg.iter().cloned().fold(f32::INFINITY, f32::min);
1371 let annual_max: f32 = monthly_avg.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1372 Self { monthly_avg, annual_mean, annual_min, annual_max }
1373 }
1374
1375 pub fn is_frozen(&self, month: usize) -> bool {
1376 self.monthly_avg[month % 12] < 0.0
1377 }
1378
1379 pub fn frost_free_months(&self) -> usize {
1380 self.monthly_avg.iter().filter(|&&t| t > 0.0).count()
1381 }
1382}
1383
1384pub struct BiomeSuccession;
1388
1389impl BiomeSuccession {
1390 pub fn successor(biome: BiomeType, years: f32) -> BiomeType {
1392 match biome {
1393 BiomeType::Badlands if years > 50.0 => BiomeType::Shrubland,
1394 BiomeType::Shrubland if years > 100.0 => BiomeType::Grassland,
1395 BiomeType::Grassland if years > 200.0 => BiomeType::TemperateForest,
1396 BiomeType::Tundra if years > 500.0 => BiomeType::Taiga,
1397 BiomeType::Taiga if years > 1000.0 => BiomeType::Boreal,
1398 BiomeType::Desert if years > 100.0 => BiomeType::Shrubland,
1399 BiomeType::Volcanic if years > 20.0 => BiomeType::Badlands,
1400 BiomeType::Beach if years > 30.0 => BiomeType::Grassland,
1401 _ => biome,
1402 }
1403 }
1404
1405 pub fn time_to_next(biome: BiomeType) -> f32 {
1407 match biome {
1408 BiomeType::Volcanic => 20.0,
1409 BiomeType::Beach => 30.0,
1410 BiomeType::Badlands => 50.0,
1411 BiomeType::Shrubland => 100.0,
1412 BiomeType::Desert => 100.0,
1413 BiomeType::Grassland => 200.0,
1414 BiomeType::Tundra => 500.0,
1415 BiomeType::Taiga => 1000.0,
1416 _ => f32::INFINITY,
1417 }
1418 }
1419}
1420
1421#[cfg(test)]
1424mod extended_biome_tests {
1425 use super::*;
1426 use crate::terrain::heightmap::FractalNoise;
1427
1428 #[test]
1429 fn test_biome_stats_from_map() {
1430 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1431 let sim = ClimateSimulator::default();
1432 let climate = sim.simulate(&hm);
1433 let bm = BiomeMap::from_heightmap(&hm, &climate);
1434 let stats = BiomeStats::from_map(&bm);
1435 assert_eq!(stats.total, 32 * 32);
1436 let total: usize = stats.counts.iter().sum();
1437 assert_eq!(total, 32 * 32);
1438 }
1439
1440 #[test]
1441 fn test_biome_stats_diversity() {
1442 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1443 let sim = ClimateSimulator::default();
1444 let climate = sim.simulate(&hm);
1445 let bm = BiomeMap::from_heightmap(&hm, &climate);
1446 let stats = BiomeStats::from_map(&bm);
1447 let div = stats.diversity_index();
1448 assert!(div >= 0.0 && div <= 1.0);
1449 }
1450
1451 #[test]
1452 fn test_biome_adjacency() {
1453 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1454 let sim = ClimateSimulator::default();
1455 let climate = sim.simulate(&hm);
1456 let bm = BiomeMap::from_heightmap(&hm, &climate);
1457 let adj = BiomeAdjacency::from_map(&bm);
1458 for i in 0..20 {
1460 for j in 0..20 {
1461 assert_eq!(adj.adjacency[i][j], adj.adjacency[j][i],
1462 "Adjacency should be symmetric");
1463 }
1464 }
1465 }
1466
1467 #[test]
1468 fn test_precipitation_pattern() {
1469 let pat = PrecipitationPattern::for_biome(BiomeType::TropicalForest);
1470 assert!(pat.annual > 1000.0, "Tropical forest should be wet");
1471 let dry = PrecipitationPattern::for_biome(BiomeType::Desert);
1472 assert!(dry.annual < 100.0, "Desert should be dry");
1473 }
1474
1475 #[test]
1476 fn test_temperature_range() {
1477 let tr = TemperatureRange::for_biome(BiomeType::Arctic);
1478 assert!(tr.annual_max < 10.0, "Arctic should be cold year-round");
1479 let tropic = TemperatureRange::for_biome(BiomeType::TropicalForest);
1480 assert!(tropic.annual_min > 20.0, "Tropical should be warm year-round");
1481 }
1482
1483 #[test]
1484 fn test_climate_zone_classification() {
1485 assert_eq!(ClimateZone::from_params(0.9, 0.9, 0.3), ClimateZone::Tropical);
1486 assert_eq!(ClimateZone::from_params(0.8, 0.1, 0.3), ClimateZone::Arid);
1487 assert_eq!(ClimateZone::from_params(0.5, 0.6, 0.3), ClimateZone::Temperate);
1488 assert_eq!(ClimateZone::from_params(0.1, 0.3, 0.3), ClimateZone::Polar);
1489 }
1490
1491 #[test]
1492 fn test_biome_succession() {
1493 assert_eq!(BiomeSuccession::successor(BiomeType::Volcanic, 25.0), BiomeType::Badlands);
1494 assert_eq!(BiomeSuccession::successor(BiomeType::Volcanic, 5.0), BiomeType::Volcanic);
1495 assert_eq!(BiomeSuccession::successor(BiomeType::Badlands, 100.0), BiomeType::Shrubland);
1496 }
1497
1498 #[test]
1499 fn test_biome_transition_map() {
1500 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1501 let sim = ClimateSimulator::default();
1502 let climate = sim.simulate(&hm);
1503 let bm = BiomeMap::from_heightmap(&hm, &climate);
1504 let tm = BiomeTransitionMap::from_map(&bm, 2);
1505 assert_eq!(tm.transitions.len(), 32 * 32);
1506 assert!(tm.transitions.iter().all(|&v| v >= 0.0 && v <= 1.0));
1507 }
1508
1509 #[test]
1510 fn test_biome_from_index_coverage() {
1511 for i in 0..20 {
1512 let b = biome_from_index(i);
1513 assert_eq!(b as usize, i);
1514 }
1515 }
1516
1517 #[test]
1518 fn test_river_simulator() {
1519 let hm = FractalNoise::generate(32, 32, 4, 2.0, 0.5, 3.0, 42);
1520 let rivers = RiverSimulator::generate(&hm, 0.9);
1521 assert_eq!(rivers.data.len(), 32 * 32);
1522 }
1523}