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    /// Calculate the distance between two parameter ranges.
147    #[inline]
148    #[must_use]
149    pub const fn distance_param(&self, target: &Parameter) -> i64 {
150        let above = target.min - self.max;
151        let below = self.min - target.max;
152        if above > 0 {
153            above
154        } else if below > 0 {
155            below
156        } else {
157            0
158        }
159    }
160
161    /// Expand this parameter to include another parameter.
162    #[must_use]
163    pub const fn span_with(&self, other: Option<&Parameter>) -> Self {
164        match other {
165            Some(o) => Self {
166                min: self.min.min(o.min),
167                max: self.max.max(o.max),
168            },
169            None => *self,
170        }
171    }
172}
173
174/// A biome's full parameter specification.
175///
176/// Contains ranges for all 6 climate parameters plus an offset value
177/// used as a tiebreaker in biome selection.
178#[derive(Debug, Clone, Copy)]
179pub struct ParameterPoint {
180    /// Temperature range
181    pub temperature: Parameter,
182    /// Humidity range
183    pub humidity: Parameter,
184    /// Continentalness range
185    pub continentalness: Parameter,
186    /// Erosion range
187    pub erosion: Parameter,
188    /// Depth range
189    pub depth: Parameter,
190    /// Weirdness range
191    pub weirdness: Parameter,
192    /// Offset (quantized) - used as tiebreaker
193    pub offset: i64,
194}
195
196impl ParameterPoint {
197    /// Create a new parameter point.
198    #[must_use]
199    pub const fn new(
200        temperature: Parameter,
201        humidity: Parameter,
202        continentalness: Parameter,
203        erosion: Parameter,
204        depth: Parameter,
205        weirdness: Parameter,
206        offset: i64,
207    ) -> Self {
208        Self {
209            temperature,
210            humidity,
211            continentalness,
212            erosion,
213            depth,
214            weirdness,
215            offset,
216        }
217    }
218
219    /// Calculate the fitness (distance) between this parameter point and a target.
220    ///
221    /// Lower fitness = better match. Uses squared distances.
222    #[must_use]
223    #[expect(
224        clippy::many_single_char_names,
225        reason = "single-letter abbreviations match vanilla's climate parameter names"
226    )]
227    pub const fn fitness(&self, target: &TargetPoint) -> i64 {
228        let t = self.temperature.distance(target.temperature);
229        let h = self.humidity.distance(target.humidity);
230        let c = self.continentalness.distance(target.continentalness);
231        let e = self.erosion.distance(target.erosion);
232        let d = self.depth.distance(target.depth);
233        let w = self.weirdness.distance(target.weirdness);
234
235        // Sum of squared distances (matches vanilla Mth.square usage)
236        t * t + h * h + c * c + e * e + d * d + w * w + self.offset * self.offset
237    }
238
239    /// Get the parameter space as a slice of parameters.
240    #[must_use]
241    pub const fn parameter_space(&self) -> [Parameter; PARAMETER_COUNT] {
242        [
243            self.temperature,
244            self.humidity,
245            self.continentalness,
246            self.erosion,
247            self.depth,
248            self.weirdness,
249            Parameter::new(self.offset, self.offset),
250        ]
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_target_point_from_floats() {
260        let target = TargetPoint::from_floats(0.5, -0.3, 0.0, 0.1, 0.0, 0.2);
261        assert_eq!(target.temperature, 5000);
262        assert_eq!(target.humidity, -3000);
263        assert_eq!(target.continentalness, 0);
264        assert_eq!(target.erosion, 1000);
265        assert_eq!(target.depth, 0);
266        assert_eq!(target.weirdness, 2000);
267    }
268
269    #[test]
270    fn test_parameter_distance() {
271        let param = Parameter::new(-5000, 5000);
272
273        // Inside range
274        assert_eq!(param.distance(0), 0);
275        assert_eq!(param.distance(5000), 0);
276        assert_eq!(param.distance(-5000), 0);
277
278        // Outside range
279        assert_eq!(param.distance(6000), 1000);
280        assert_eq!(param.distance(-6000), 1000);
281        assert_eq!(param.distance(10000), 5000);
282    }
283
284    #[test]
285    fn test_parameter_point_fitness() {
286        let params = ParameterPoint::new(
287            Parameter::new(0, 0),
288            Parameter::new(0, 0),
289            Parameter::new(0, 0),
290            Parameter::new(0, 0),
291            Parameter::new(0, 0),
292            Parameter::new(0, 0),
293            0,
294        );
295
296        // Perfect match
297        let target = TargetPoint::new(0, 0, 0, 0, 0, 0);
298        assert_eq!(params.fitness(&target), 0);
299
300        // Off by 100 in temperature
301        let target = TargetPoint::new(100, 0, 0, 0, 0, 0);
302        assert_eq!(params.fitness(&target), 100 * 100);
303
304        // Off by 100 in two parameters
305        let target = TargetPoint::new(100, 100, 0, 0, 0, 0);
306        assert_eq!(params.fitness(&target), 100 * 100 + 100 * 100);
307    }
308}