Skip to main content

Horizon_Network_Common/
spatial.rs

1//! Spatial types for 3D world coordinates and region boundaries.
2//!
3//! These types are used across all Horizon ecosystem components to represent
4//! positions in the game world and define region boundaries.
5
6use serde::{Deserialize, Serialize};
7
8/// 3D world coordinates using f64 for precision.
9///
10/// This type represents a point in the game world with double-precision
11/// floating point values for maximum accuracy in large worlds.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
13pub struct WorldCoordinate {
14    pub x: f64,
15    pub y: f64,
16    pub z: f64,
17}
18
19impl WorldCoordinate {
20    /// Creates a new world coordinate.
21    pub fn new(x: f64, y: f64, z: f64) -> Self {
22        Self { x, y, z }
23    }
24
25    /// Creates a zero coordinate (origin).
26    pub fn zero() -> Self {
27        Self::new(0.0, 0.0, 0.0)
28    }
29
30    /// Calculate 3D Euclidean distance to another coordinate.
31    pub fn distance_to(&self, other: &WorldCoordinate) -> f64 {
32        let dx = self.x - other.x;
33        let dy = self.y - other.y;
34        let dz = self.z - other.z;
35        (dx * dx + dy * dy + dz * dz).sqrt()
36    }
37
38    /// Calculate 3D vector to another coordinate.
39    pub fn vector_to(&self, other: &WorldCoordinate) -> WorldCoordinate {
40        WorldCoordinate {
41            x: other.x - self.x,
42            y: other.y - self.y,
43            z: other.z - self.z,
44        }
45    }
46
47    /// Calculate magnitude (length) of this coordinate as a vector.
48    pub fn magnitude(&self) -> f64 {
49        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
50    }
51
52    /// Normalize this coordinate as a unit vector.
53    pub fn normalized(&self) -> WorldCoordinate {
54        let mag = self.magnitude();
55        if mag == 0.0 {
56            WorldCoordinate::zero()
57        } else {
58            WorldCoordinate {
59                x: self.x / mag,
60                y: self.y / mag,
61                z: self.z / mag,
62            }
63        }
64    }
65
66    /// Add another coordinate (vector addition).
67    pub fn add(&self, other: &WorldCoordinate) -> WorldCoordinate {
68        WorldCoordinate {
69            x: self.x + other.x,
70            y: self.y + other.y,
71            z: self.z + other.z,
72        }
73    }
74
75    /// Scale coordinate by a factor.
76    pub fn scale(&self, factor: f64) -> WorldCoordinate {
77        WorldCoordinate {
78            x: self.x * factor,
79            y: self.y * factor,
80            z: self.z * factor,
81        }
82    }
83
84    /// Creates from environment variables (HORIZON_CENTER_X/Y/Z).
85    pub fn from_env() -> Self {
86        let x = std::env::var("HORIZON_CENTER_X")
87            .ok()
88            .and_then(|s| s.parse().ok())
89            .unwrap_or(0.0);
90        let y = std::env::var("HORIZON_CENTER_Y")
91            .ok()
92            .and_then(|s| s.parse().ok())
93            .unwrap_or(0.0);
94        let z = std::env::var("HORIZON_CENTER_Z")
95            .ok()
96            .and_then(|s| s.parse().ok())
97            .unwrap_or(0.0);
98        Self { x, y, z }
99    }
100}
101
102/// Server region coordinates (i64 for grid-based regions).
103///
104/// This type represents a region's position in a discrete 3D grid,
105/// where each cell can contain one server instance.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
107pub struct RegionCoordinate {
108    pub x: i64,
109    pub y: i64,
110    pub z: i64,
111}
112
113impl RegionCoordinate {
114    /// Creates a new region coordinate.
115    pub fn new(x: i64, y: i64, z: i64) -> Self {
116        Self { x, y, z }
117    }
118
119    /// Center region coordinate (0, 0, 0).
120    pub fn center() -> Self {
121        Self::new(0, 0, 0)
122    }
123
124    /// Calculate Manhattan distance to another region.
125    pub fn manhattan_distance(&self, other: &RegionCoordinate) -> i64 {
126        (self.x - other.x).abs() + (self.y - other.y).abs() + (self.z - other.z).abs()
127    }
128
129    /// Get adjacent region coordinates (6 directions in 3D).
130    pub fn adjacent_regions(&self) -> Vec<RegionCoordinate> {
131        vec![
132            RegionCoordinate::new(self.x + 1, self.y, self.z),
133            RegionCoordinate::new(self.x - 1, self.y, self.z),
134            RegionCoordinate::new(self.x, self.y + 1, self.z),
135            RegionCoordinate::new(self.x, self.y - 1, self.z),
136            RegionCoordinate::new(self.x, self.y, self.z + 1),
137            RegionCoordinate::new(self.x, self.y, self.z - 1),
138        ]
139    }
140
141    /// Convert region coordinate to world coordinate center.
142    ///
143    /// Uses the region size to calculate the center point of this region.
144    pub fn to_world_center(&self, region_size: f64) -> WorldCoordinate {
145        WorldCoordinate::new(
146            self.x as f64 * region_size,
147            self.y as f64 * region_size,
148            self.z as f64 * region_size,
149        )
150    }
151
152    /// Calculate which region a world coordinate belongs to.
153    pub fn from_world_coordinate(coord: &WorldCoordinate, region_size: f64) -> Self {
154        Self {
155            x: (coord.x / region_size).floor() as i64,
156            y: (coord.y / region_size).floor() as i64,
157            z: (coord.z / region_size).floor() as i64,
158        }
159    }
160
161    /// Creates from environment variables (HORIZON_REGION_X/Y/Z).
162    pub fn from_env() -> Self {
163        let x = std::env::var("HORIZON_REGION_X")
164            .ok()
165            .and_then(|s| s.parse().ok())
166            .unwrap_or(0);
167        let y = std::env::var("HORIZON_REGION_Y")
168            .ok()
169            .and_then(|s| s.parse().ok())
170            .unwrap_or(0);
171        let z = std::env::var("HORIZON_REGION_Z")
172            .ok()
173            .and_then(|s| s.parse().ok())
174            .unwrap_or(0);
175        Self { x, y, z }
176    }
177}
178
179/// Defines the spatial boundaries of a game region.
180///
181/// This structure defines a 3D axis-aligned bounding box (AABB) that encompasses
182/// all the space within a game region. Compatible with both Horizon and Atlas.
183#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
184pub struct RegionBounds {
185    /// Minimum X coordinate (western boundary)
186    pub min_x: f64,
187    /// Maximum X coordinate (eastern boundary)
188    pub max_x: f64,
189    /// Minimum Y coordinate (bottom boundary)
190    pub min_y: f64,
191    /// Maximum Y coordinate (top boundary)
192    pub max_y: f64,
193    /// Minimum Z coordinate (southern boundary)
194    pub min_z: f64,
195    /// Maximum Z coordinate (northern boundary)
196    pub max_z: f64,
197}
198
199impl Default for RegionBounds {
200    fn default() -> Self {
201        Self {
202            min_x: -1000.0,
203            max_x: 1000.0,
204            min_y: -1000.0,
205            max_y: 1000.0,
206            min_z: -1000.0,
207            max_z: 1000.0,
208        }
209    }
210}
211
212impl RegionBounds {
213    /// Creates a new region bounds from min/max values.
214    pub fn new(min_x: f64, max_x: f64, min_y: f64, max_y: f64, min_z: f64, max_z: f64) -> Self {
215        Self { min_x, max_x, min_y, max_y, min_z, max_z }
216    }
217
218    /// Creates region bounds from center point and half-extents.
219    pub fn from_center(center: WorldCoordinate, half_extent: f64) -> Self {
220        Self {
221            min_x: center.x - half_extent,
222            max_x: center.x + half_extent,
223            min_y: center.y - half_extent,
224            max_y: center.y + half_extent,
225            min_z: center.z - half_extent,
226            max_z: center.z + half_extent,
227        }
228    }
229
230    /// Get the center point of this region.
231    pub fn center(&self) -> WorldCoordinate {
232        WorldCoordinate::new(
233            (self.min_x + self.max_x) / 2.0,
234            (self.min_y + self.max_y) / 2.0,
235            (self.min_z + self.max_z) / 2.0,
236        )
237    }
238
239    /// Get the half-extent (assuming cubic region).
240    pub fn half_extent(&self) -> f64 {
241        (self.max_x - self.min_x) / 2.0
242    }
243
244    /// Check if a world coordinate is within these bounds.
245    pub fn contains(&self, coord: &WorldCoordinate) -> bool {
246        coord.x >= self.min_x && coord.x <= self.max_x &&
247        coord.y >= self.min_y && coord.y <= self.max_y &&
248        coord.z >= self.min_z && coord.z <= self.max_z
249    }
250
251    /// Calculate the distance from a point to the nearest boundary.
252    /// Returns negative if inside, positive if outside.
253    pub fn distance_to_boundary(&self, coord: &WorldCoordinate) -> f64 {
254        let dx = (coord.x - self.min_x).min(self.max_x - coord.x);
255        let dy = (coord.y - self.min_y).min(self.max_y - coord.y);
256        let dz = (coord.z - self.min_z).min(self.max_z - coord.z);
257        dx.min(dy).min(dz)
258    }
259
260    /// Check if this region overlaps with another.
261    pub fn overlaps(&self, other: &RegionBounds) -> bool {
262        self.min_x <= other.max_x && self.max_x >= other.min_x &&
263        self.min_y <= other.max_y && self.max_y >= other.min_y &&
264        self.min_z <= other.max_z && self.max_z >= other.min_z
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_world_coordinate_distance() {
274        let a = WorldCoordinate::new(0.0, 0.0, 0.0);
275        let b = WorldCoordinate::new(3.0, 4.0, 0.0);
276        assert!((a.distance_to(&b) - 5.0).abs() < 0.0001);
277    }
278
279    #[test]
280    fn test_region_bounds_contains() {
281        let bounds = RegionBounds::from_center(WorldCoordinate::zero(), 100.0);
282        assert!(bounds.contains(&WorldCoordinate::new(0.0, 0.0, 0.0)));
283        assert!(bounds.contains(&WorldCoordinate::new(99.0, 0.0, 0.0)));
284        assert!(!bounds.contains(&WorldCoordinate::new(101.0, 0.0, 0.0)));
285    }
286
287    #[test]
288    fn test_region_coordinate_conversion() {
289        let world = WorldCoordinate::new(150.0, 50.0, -25.0);
290        let region = RegionCoordinate::from_world_coordinate(&world, 100.0);
291        assert_eq!(region, RegionCoordinate::new(1, 0, -1));
292    }
293}