1use super::Grid2D;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum Biome {
11 Ocean,
12 DeepOcean,
13 CoralReef,
14 Beach,
15 TropicalRainforest,
16 TropicalDryForest,
17 Savanna,
18 Desert,
19 HotSteppe,
20 Mediterranean,
21 TemperateForest,
22 TemperateRainforest,
23 Grassland,
24 ColdSteppe,
25 BorealForest, Tundra,
27 IceCap,
28 Marsh,
29 Mangrove,
30 Alpine,
31 VolcanicWaste,
32 River,
33 Lake,
34}
35
36impl Biome {
37 pub fn color(self) -> [f32; 4] {
39 match self {
40 Self::Ocean => [0.1, 0.2, 0.6, 1.0],
41 Self::DeepOcean => [0.05, 0.1, 0.4, 1.0],
42 Self::CoralReef => [0.2, 0.5, 0.6, 1.0],
43 Self::Beach => [0.9, 0.85, 0.6, 1.0],
44 Self::TropicalRainforest => [0.05, 0.5, 0.1, 1.0],
45 Self::TropicalDryForest => [0.3, 0.5, 0.15, 1.0],
46 Self::Savanna => [0.6, 0.65, 0.2, 1.0],
47 Self::Desert => [0.85, 0.75, 0.45, 1.0],
48 Self::HotSteppe => [0.7, 0.6, 0.3, 1.0],
49 Self::Mediterranean => [0.5, 0.6, 0.25, 1.0],
50 Self::TemperateForest => [0.15, 0.55, 0.15, 1.0],
51 Self::TemperateRainforest => [0.1, 0.45, 0.2, 1.0],
52 Self::Grassland => [0.45, 0.6, 0.2, 1.0],
53 Self::ColdSteppe => [0.55, 0.55, 0.35, 1.0],
54 Self::BorealForest => [0.15, 0.35, 0.2, 1.0],
55 Self::Tundra => [0.6, 0.65, 0.6, 1.0],
56 Self::IceCap => [0.9, 0.92, 0.95, 1.0],
57 Self::Marsh => [0.3, 0.4, 0.25, 1.0],
58 Self::Mangrove => [0.2, 0.4, 0.15, 1.0],
59 Self::Alpine => [0.5, 0.5, 0.5, 1.0],
60 Self::VolcanicWaste => [0.3, 0.2, 0.2, 1.0],
61 Self::River => [0.15, 0.3, 0.7, 1.0],
62 Self::Lake => [0.2, 0.35, 0.65, 1.0],
63 }
64 }
65
66 pub fn is_water(self) -> bool {
68 matches!(self, Self::Ocean | Self::DeepOcean | Self::CoralReef | Self::River | Self::Lake)
69 }
70
71 pub fn habitability(self) -> f32 {
73 match self {
74 Self::TemperateForest | Self::Grassland | Self::Mediterranean => 0.9,
75 Self::TemperateRainforest | Self::Savanna | Self::TropicalDryForest => 0.7,
76 Self::TropicalRainforest | Self::Marsh => 0.5,
77 Self::BorealForest | Self::ColdSteppe | Self::HotSteppe => 0.4,
78 Self::Beach | Self::Mangrove => 0.3,
79 Self::Desert | Self::Tundra | Self::Alpine => 0.15,
80 Self::IceCap | Self::VolcanicWaste => 0.02,
81 _ => 0.0, }
83 }
84
85 pub fn resources(self) -> f32 {
87 match self {
88 Self::TropicalRainforest | Self::TemperateRainforest => 0.9,
89 Self::TemperateForest | Self::BorealForest => 0.7,
90 Self::Grassland | Self::Savanna => 0.5,
91 Self::Mediterranean | Self::Marsh => 0.6,
92 Self::Desert | Self::Tundra => 0.1,
93 Self::Alpine | Self::VolcanicWaste => 0.2,
94 _ => 0.3,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct BiomeMap {
102 pub width: usize,
103 pub height: usize,
104 pub biomes: Vec<Biome>,
105}
106
107impl BiomeMap {
108 pub fn biome_at(&self, x: usize, y: usize) -> Biome {
109 self.biomes[y * self.width + x]
110 }
111
112 pub fn distribution(&self) -> std::collections::HashMap<Biome, usize> {
114 let mut map = std::collections::HashMap::new();
115 for &b in &self.biomes {
116 *map.entry(b).or_insert(0) += 1;
117 }
118 map
119 }
120
121 pub fn find_biome(&self, target: Biome) -> Vec<(usize, usize)> {
123 let mut cells = Vec::new();
124 for y in 0..self.height {
125 for x in 0..self.width {
126 if self.biome_at(x, y) == target {
127 cells.push((x, y));
128 }
129 }
130 }
131 cells
132 }
133}
134
135pub fn classify(
137 heightmap: &Grid2D,
138 temperature: &Grid2D,
139 precipitation: &Grid2D,
140 sea_level: f32,
141) -> BiomeMap {
142 let w = heightmap.width;
143 let h = heightmap.height;
144 let mut biomes = Vec::with_capacity(w * h);
145
146 for y in 0..h {
147 for x in 0..w {
148 let elev = heightmap.get(x, y);
149 let temp = temperature.get(x, y);
150 let precip = precipitation.get(x, y);
151
152 let biome = if elev < sea_level - 0.15 {
153 Biome::DeepOcean
154 } else if elev < sea_level - 0.02 {
155 if temp > 22.0 && precip > 0.3 {
156 Biome::CoralReef
157 } else {
158 Biome::Ocean
159 }
160 } else if elev < sea_level + 0.02 {
161 if temp > 20.0 && precip > 0.5 {
162 Biome::Mangrove
163 } else {
164 Biome::Beach
165 }
166 } else if elev > 0.85 {
167 if temp < -5.0 {
168 Biome::IceCap
169 } else {
170 Biome::Alpine
171 }
172 } else {
173 whittaker_classify(temp, precip)
175 };
176
177 biomes.push(biome);
178 }
179 }
180
181 BiomeMap { width: w, height: h, biomes }
182}
183
184fn whittaker_classify(temp: f32, precip: f32) -> Biome {
186 if temp < -10.0 {
187 if precip > 0.3 { Biome::IceCap } else { Biome::Tundra }
188 } else if temp < 0.0 {
189 if precip > 0.4 { Biome::BorealForest } else { Biome::Tundra }
190 } else if temp < 5.0 {
191 if precip > 0.5 { Biome::BorealForest } else { Biome::ColdSteppe }
192 } else if temp < 15.0 {
193 if precip > 0.7 { Biome::TemperateRainforest }
194 else if precip > 0.4 { Biome::TemperateForest }
195 else if precip > 0.2 { Biome::Grassland }
196 else { Biome::ColdSteppe }
197 } else if temp < 22.0 {
198 if precip > 0.6 { Biome::TemperateForest }
199 else if precip > 0.3 { Biome::Mediterranean }
200 else if precip > 0.15 { Biome::HotSteppe }
201 else { Biome::Desert }
202 } else {
203 if precip > 0.7 { Biome::TropicalRainforest }
205 else if precip > 0.4 { Biome::TropicalDryForest }
206 else if precip > 0.2 { Biome::Savanna }
207 else if precip > 0.1 { Biome::HotSteppe }
208 else { Biome::Desert }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_whittaker_hot_wet() {
218 assert_eq!(whittaker_classify(28.0, 0.8), Biome::TropicalRainforest);
219 }
220
221 #[test]
222 fn test_whittaker_cold_dry() {
223 assert_eq!(whittaker_classify(-15.0, 0.1), Biome::Tundra);
224 }
225
226 #[test]
227 fn test_whittaker_hot_dry() {
228 assert_eq!(whittaker_classify(25.0, 0.05), Biome::Desert);
229 }
230
231 #[test]
232 fn test_classify_ocean() {
233 let hm = Grid2D::filled(4, 4, 0.1); let temp = Grid2D::filled(4, 4, 20.0);
235 let precip = Grid2D::filled(4, 4, 0.5);
236 let bm = classify(&hm, &temp, &precip, 0.4);
237 assert!(bm.biome_at(0, 0).is_water());
238 }
239
240 #[test]
241 fn test_biome_habitability() {
242 assert!(Biome::TemperateForest.habitability() > Biome::Desert.habitability());
243 assert!(Biome::Ocean.habitability() == 0.0);
244 }
245}