Skip to main content

steel_utils/climate/
types.rs

1//! Climate types for biome selection.
2
3use super::{PARAMETER_COUNT, QUANTIZATION_FACTOR, quantize_coord};
4
5/// A target point representing sampled climate values.
6///
7/// All values are quantized (multiplied by 10000) to match vanilla's integer-based
8/// distance calculations. This avoids floating-point precision issues in biome lookup.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct TargetPoint {
11    /// Temperature parameter
12    pub temperature: i64,
13    /// Humidity/vegetation parameter
14    pub humidity: i64,
15    /// Continentalness parameter (inland vs ocean)
16    pub continentalness: i64,
17    /// Erosion parameter
18    pub erosion: i64,
19    /// Depth parameter (surface vs underground)
20    pub depth: i64,
21    /// Weirdness/ridges parameter
22    pub weirdness: i64,
23}
24
25impl TargetPoint {
26    /// Create a new target point with quantized values.
27    #[must_use]
28    pub const fn new(
29        temperature: i64,
30        humidity: i64,
31        continentalness: i64,
32        erosion: i64,
33        depth: i64,
34        weirdness: i64,
35    ) -> Self {
36        Self {
37            temperature,
38            humidity,
39            continentalness,
40            erosion,
41            depth,
42            weirdness,
43        }
44    }
45
46    /// Create a target point from f64 values (will be quantized).
47    #[must_use]
48    pub fn from_floats(
49        temperature: f64,
50        humidity: f64,
51        continentalness: f64,
52        erosion: f64,
53        depth: f64,
54        weirdness: f64,
55    ) -> Self {
56        Self {
57            temperature: quantize_coord(temperature),
58            humidity: quantize_coord(humidity),
59            continentalness: quantize_coord(continentalness),
60            erosion: quantize_coord(erosion),
61            depth: quantize_coord(depth),
62            weirdness: quantize_coord(weirdness),
63        }
64    }
65
66    /// Convert to a 7-element array for tree lookups.
67    /// The 7th element is always 0 (offset position).
68    #[must_use]
69    pub const fn to_parameter_array(self) -> [i64; PARAMETER_COUNT] {
70        [
71            self.temperature,
72            self.humidity,
73            self.continentalness,
74            self.erosion,
75            self.depth,
76            self.weirdness,
77            0, // Offset target is always 0
78        ]
79    }
80}
81
82/// A parameter range for biome matching.
83///
84/// Represents a range [min, max] that a climate parameter can match.
85/// A point matches if it falls within this range; distance is 0 inside
86/// and increases linearly outside.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct Parameter {
89    /// Minimum value (quantized)
90    pub min: i64,
91    /// Maximum value (quantized)
92    pub max: i64,
93}
94
95impl Parameter {
96    /// Create a new parameter range.
97    #[must_use]
98    pub const fn new(min: i64, max: i64) -> Self {
99        Self { min, max }
100    }
101
102    /// Create a point parameter (min == max).
103    #[must_use]
104    pub fn point(value: f32) -> Self {
105        Self::span(value, value)
106    }
107
108    /// Create a parameter span from float values.
109    #[must_use]
110    pub fn span(min: f32, max: f32) -> Self {
111        debug_assert!(min <= max, "min > max: {min} > {max}");
112        Self {
113            min: (min * QUANTIZATION_FACTOR) as i64,
114            max: (max * QUANTIZATION_FACTOR) as i64,
115        }
116    }
117
118    /// Create a parameter span from two parameters.
119    #[must_use]
120    pub const fn span_params(min: &Parameter, max: &Parameter) -> Self {
121        debug_assert!(min.min <= max.max, "span_params: min > max");
122        Self {
123            min: min.min,
124            max: max.max,
125        }
126    }
127
128    /// Calculate the distance from a target value to this parameter range.
129    ///
130    /// Returns 0 if the target is within the range, otherwise the distance
131    /// to the nearest edge.
132    #[inline]
133    #[must_use]
134    pub const fn distance(&self, target: i64) -> i64 {
135        let above = target - self.max;
136        let below = self.min - target;
137        if above > 0 {
138            above
139        } else if below > 0 {
140            below
141        } else {
142            0
143        }
144    }
145
146    /// Expand this parameter to include another parameter.
147    #[must_use]
148    pub const fn span_with(&self, other: Option<&Parameter>) -> Self {
149        match other {
150            Some(o) => Self {
151                min: self.min.min(o.min),
152                max: self.max.max(o.max),
153            },
154            None => *self,
155        }
156    }
157}
158
159/// A biome's full parameter specification.
160///
161/// Contains ranges for all 6 climate parameters plus an offset value
162/// used as a tiebreaker in biome selection.
163#[derive(Debug, Clone, Copy)]
164pub struct ParameterPoint {
165    /// Temperature range
166    pub temperature: Parameter,
167    /// Humidity range
168    pub humidity: Parameter,
169    /// Continentalness range
170    pub continentalness: Parameter,
171    /// Erosion range
172    pub erosion: Parameter,
173    /// Depth range
174    pub depth: Parameter,
175    /// Weirdness range
176    pub weirdness: Parameter,
177    /// Offset (quantized) - used as tiebreaker
178    pub offset: i64,
179}
180
181impl ParameterPoint {
182    /// Create a new parameter point.
183    #[must_use]
184    pub const fn new(
185        temperature: Parameter,
186        humidity: Parameter,
187        continentalness: Parameter,
188        erosion: Parameter,
189        depth: Parameter,
190        weirdness: Parameter,
191        offset: i64,
192    ) -> Self {
193        Self {
194            temperature,
195            humidity,
196            continentalness,
197            erosion,
198            depth,
199            weirdness,
200            offset,
201        }
202    }
203
204    /// Calculate the fitness (distance) between this parameter point and a target.
205    ///
206    /// Lower fitness = better match. Uses squared distances.
207    #[must_use]
208    #[expect(
209        clippy::many_single_char_names,
210        reason = "single-letter abbreviations match vanilla's climate parameter names"
211    )]
212    pub const fn fitness(&self, target: &TargetPoint) -> i64 {
213        let t = self.temperature.distance(target.temperature);
214        let h = self.humidity.distance(target.humidity);
215        let c = self.continentalness.distance(target.continentalness);
216        let e = self.erosion.distance(target.erosion);
217        let d = self.depth.distance(target.depth);
218        let w = self.weirdness.distance(target.weirdness);
219
220        // Sum of squared distances (matches vanilla Mth.square usage)
221        t * t + h * h + c * c + e * e + d * d + w * w + self.offset * self.offset
222    }
223
224    /// Get the parameter space as a slice of parameters.
225    #[must_use]
226    pub const fn parameter_space(&self) -> [Parameter; PARAMETER_COUNT] {
227        [
228            self.temperature,
229            self.humidity,
230            self.continentalness,
231            self.erosion,
232            self.depth,
233            self.weirdness,
234            Parameter::new(self.offset, self.offset),
235        ]
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_target_point_from_floats() {
245        let target = TargetPoint::from_floats(0.5, -0.3, 0.0, 0.1, 0.0, 0.2);
246        assert_eq!(target.temperature, 5000);
247        assert_eq!(target.humidity, -3000);
248        assert_eq!(target.continentalness, 0);
249        assert_eq!(target.erosion, 1000);
250        assert_eq!(target.depth, 0);
251        assert_eq!(target.weirdness, 2000);
252    }
253
254    #[test]
255    fn test_parameter_distance() {
256        let param = Parameter::new(-5000, 5000);
257
258        // Inside range
259        assert_eq!(param.distance(0), 0);
260        assert_eq!(param.distance(5000), 0);
261        assert_eq!(param.distance(-5000), 0);
262
263        // Outside range
264        assert_eq!(param.distance(6000), 1000);
265        assert_eq!(param.distance(-6000), 1000);
266        assert_eq!(param.distance(10000), 5000);
267    }
268
269    #[test]
270    fn test_parameter_point_fitness() {
271        let params = ParameterPoint::new(
272            Parameter::new(0, 0),
273            Parameter::new(0, 0),
274            Parameter::new(0, 0),
275            Parameter::new(0, 0),
276            Parameter::new(0, 0),
277            Parameter::new(0, 0),
278            0,
279        );
280
281        // Perfect match
282        let target = TargetPoint::new(0, 0, 0, 0, 0, 0);
283        assert_eq!(params.fitness(&target), 0);
284
285        // Off by 100 in temperature
286        let target = TargetPoint::new(100, 0, 0, 0, 0, 0);
287        assert_eq!(params.fitness(&target), 100 * 100);
288
289        // Off by 100 in two parameters
290        let target = TargetPoint::new(100, 100, 0, 0, 0, 0);
291        assert_eq!(params.fitness(&target), 100 * 100 + 100 * 100);
292    }
293}