|
| 1 | +use std::time::Instant; |
| 2 | +use tokio::task::JoinHandle; |
| 3 | + |
| 4 | +/// Calculate Euclidean distance between two 3D points (x1,y1,z1) -> (x2,y2,z2) |
| 5 | +/// Returns None if coordinate subtraction overflows |
| 6 | +pub fn calculate_distance(x1: i32, y1: i32, z1: i32, x2: i32, y2: i32, z2: i32) -> Option<f64> { |
| 7 | + let dx = x2.checked_sub(x1)?; |
| 8 | + let dy = y2.checked_sub(y1)?; |
| 9 | + let dz = z2.checked_sub(z1)?; |
| 10 | + |
| 11 | + let dx = f64::from(dx); |
| 12 | + let dy = f64::from(dy); |
| 13 | + let dz = f64::from(dz); |
| 14 | + |
| 15 | + Some((dx * dx + dy * dy + dz * dz).sqrt()) |
| 16 | +} |
| 17 | + |
| 18 | +/// Represents the current movement state of a player |
| 19 | +#[derive(Debug)] |
| 20 | +pub struct MovementState { |
| 21 | + /// Starting position (x, y, z) |
| 22 | + pub source_x: i32, |
| 23 | + pub source_y: i32, |
| 24 | + pub source_z: i32, |
| 25 | + |
| 26 | + /// Destination position (x, y, z) |
| 27 | + pub dest_x: i32, |
| 28 | + pub dest_y: i32, |
| 29 | + pub dest_z: i32, |
| 30 | + |
| 31 | + /// When the movement started |
| 32 | + pub start_time: Instant, |
| 33 | + |
| 34 | + /// Movement speed in game units per second |
| 35 | + pub speed: f64, |
| 36 | + |
| 37 | + /// Handle to the periodic broadcast task |
| 38 | + pub task_handle: Option<JoinHandle<()>>, |
| 39 | +} |
| 40 | + |
| 41 | +impl MovementState { |
| 42 | + /// Create a new movement state |
| 43 | + pub fn new( |
| 44 | + source_x: i32, |
| 45 | + source_y: i32, |
| 46 | + source_z: i32, |
| 47 | + dest_x: i32, |
| 48 | + dest_y: i32, |
| 49 | + dest_z: i32, |
| 50 | + speed: u16, |
| 51 | + ) -> Self { |
| 52 | + Self { |
| 53 | + source_x, |
| 54 | + source_y, |
| 55 | + source_z, |
| 56 | + dest_x, |
| 57 | + dest_y, |
| 58 | + dest_z, |
| 59 | + start_time: Instant::now(), |
| 60 | + speed: f64::from(speed), |
| 61 | + task_handle: None, |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + /// Calculate the total distance to travel |
| 66 | + pub fn total_distance(&self) -> Option<f64> { |
| 67 | + calculate_distance( |
| 68 | + self.source_x, |
| 69 | + self.source_y, |
| 70 | + self.source_z, |
| 71 | + self.dest_x, |
| 72 | + self.dest_y, |
| 73 | + self.dest_z, |
| 74 | + ) |
| 75 | + } |
| 76 | + |
| 77 | + /// Calculate how long the entire journey should take (in seconds) |
| 78 | + pub fn calculate_travel_duration(&self) -> f64 { |
| 79 | + let Some(distance) = self.total_distance() else { |
| 80 | + return 0.0; |
| 81 | + }; |
| 82 | + if self.speed > 0.0 { |
| 83 | + distance / self.speed |
| 84 | + } else { |
| 85 | + 0.0 |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + /// Calculate the current interpolated position based on elapsed time |
| 90 | + pub fn calculate_current_position(&self) -> (i32, i32, i32) { |
| 91 | + let elapsed = self.start_time.elapsed().as_secs_f64(); |
| 92 | + let duration = self.calculate_travel_duration(); |
| 93 | + |
| 94 | + if duration <= 0.0 || elapsed >= duration { |
| 95 | + // Already arrived or instant movement |
| 96 | + return (self.dest_x, self.dest_y, self.dest_z); |
| 97 | + } |
| 98 | + |
| 99 | + let progress = (elapsed / duration).min(1.0); |
| 100 | + |
| 101 | + let current_x = |
| 102 | + self.source_x + ((f64::from(self.dest_x) - f64::from(self.source_x)) * progress) as i32; |
| 103 | + let current_y = |
| 104 | + self.source_y + ((f64::from(self.dest_y) - f64::from(self.source_y)) * progress) as i32; |
| 105 | + let current_z = |
| 106 | + self.source_z + ((f64::from(self.dest_z) - f64::from(self.source_z)) * progress) as i32; |
| 107 | + |
| 108 | + (current_x, current_y, current_z) |
| 109 | + } |
| 110 | + |
| 111 | + /// Check if the player has arrived at the destination |
| 112 | + pub fn has_arrived(&self) -> bool { |
| 113 | + let elapsed = self.start_time.elapsed().as_secs_f64(); |
| 114 | + let duration = self.calculate_travel_duration(); |
| 115 | + elapsed >= duration |
| 116 | + } |
| 117 | + |
| 118 | + /// Cancel the periodic broadcast task if it exists |
| 119 | + pub fn cancel_task(&mut self) { |
| 120 | + if let Some(handle) = self.task_handle.take() { |
| 121 | + handle.abort(); |
| 122 | + } |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +impl Drop for MovementState { |
| 127 | + fn drop(&mut self) { |
| 128 | + self.cancel_task(); |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +#[cfg(test)] |
| 133 | +mod tests { |
| 134 | + use super::*; |
| 135 | + |
| 136 | + #[test] |
| 137 | + fn test_calculate_distance() { |
| 138 | + let state = MovementState::new(0, 0, 0, 300, 400, 0, 100); |
| 139 | + let distance = state.total_distance().unwrap(); |
| 140 | + //float values can give 0.00000001 error, so we check for a range of 0.001 |
| 141 | + assert!((distance - 500.0).abs() < 0.001); // 3-4-5 triangle |
| 142 | + } |
| 143 | + |
| 144 | + #[test] |
| 145 | + fn test_calculate_travel_duration() { |
| 146 | + let state = MovementState::new(0, 0, 0, 500, 0, 0, 100); |
| 147 | + let duration = state.calculate_travel_duration(); |
| 148 | + //float values can give 0.00000001 error, so we check for a range of 0.001 |
| 149 | + assert!((duration - 5.0).abs() < 0.1); // 500 units at 100 units/sec = 5 seconds |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn test_calculate_current_position_at_start() { |
| 154 | + let state = MovementState::new(0, 0, 0, 1000, 0, 0, 100); |
| 155 | + let (x, y, z) = state.calculate_current_position(); |
| 156 | + // Should be at or very near start position |
| 157 | + assert!((0..10).contains(&x)); // Allow small movement |
| 158 | + assert_eq!(y, 0); |
| 159 | + assert_eq!(z, 0); |
| 160 | + } |
| 161 | + |
| 162 | + #[test] |
| 163 | + fn test_has_arrived_immediately() { |
| 164 | + let state = MovementState::new(0, 0, 0, 0, 0, 0, 100); |
| 165 | + assert!(state.has_arrived()); // No distance = instant arrival |
| 166 | + } |
| 167 | +} |
0 commit comments