cs2_nav/
position.rs

1use crate::constants::{CROUCH_JUMP_HEIGHT_GAIN, GRAVITY, PLAYER_WIDTH, RUNNING_SPEED, jump_speed};
2
3use geo::geometry::Point;
4use pyo3::{FromPyObject, Py, PyRef, PyRefMut, PyResult, pyclass, pyfunction, pymethods};
5use serde::{Deserialize, Serialize};
6use std::ops::{Add, Div, Mul, Sub};
7
8#[pyclass(eq, module = "cs2_nav")]
9#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
10pub struct Position {
11    #[pyo3(get, set)]
12    pub x: f64,
13    #[pyo3(get, set)]
14    pub y: f64,
15    #[pyo3(get, set)]
16    pub z: f64,
17}
18
19impl Position {
20    #[must_use]
21    pub fn to_point_2d(self) -> Point {
22        Point::new(self.x, self.y)
23    }
24}
25
26impl Add for Position {
27    type Output = Self;
28
29    fn add(self, other: Self) -> Self {
30        Self::new(self.x + other.x, self.y + other.y, self.z + other.z)
31    }
32}
33
34impl Sub for Position {
35    type Output = Self;
36
37    fn sub(self, other: Self) -> Self {
38        Self::new(self.x - other.x, self.y - other.y, self.z - other.z)
39    }
40}
41
42impl Mul<f64> for Position {
43    type Output = Self;
44
45    fn mul(self, other: f64) -> Self {
46        Self::new(self.x * other, self.y * other, self.z * other)
47    }
48}
49
50impl Div<f64> for Position {
51    type Output = Self;
52
53    fn div(self, other: f64) -> Self {
54        Self::new(self.x / other, self.y / other, self.z / other)
55    }
56}
57
58#[derive(FromPyObject)]
59pub(crate) enum PositionFromInputOptions {
60    #[pyo3(transparent)]
61    Other(Vec<f64>),
62    #[pyo3(transparent)]
63    Position(Position),
64}
65
66#[pymethods]
67impl Position {
68    #[must_use]
69    #[new]
70    pub const fn new(x: f64, y: f64, z: f64) -> Self {
71        Self { x, y, z }
72    }
73
74    #[staticmethod]
75    pub(crate) fn from_input(value: PositionFromInputOptions) -> PyResult<Self> {
76        match value {
77            PositionFromInputOptions::Position(pos) => Ok(pos),
78            PositionFromInputOptions::Other(input) => {
79                if input.len() != 3 {
80                    return Err(pyo3::exceptions::PyValueError::new_err(
81                        "Input must be a Vector3 or tuple or list of length 3",
82                    ));
83                }
84                Ok(Self::new(input[0], input[1], input[2]))
85            }
86        }
87    }
88
89    #[must_use]
90    pub fn __sub__(&self, other: &Self) -> Self {
91        *self - *other
92    }
93
94    #[must_use]
95    #[allow(clippy::missing_const_for_fn)]
96    pub fn __add__(&self, other: &Self) -> Self {
97        *self + *other
98    }
99
100    #[must_use]
101    pub fn __mul__(&self, other: f64) -> Self {
102        *self * other
103    }
104
105    /// Python division operator
106    ///
107    /// # Errors
108    ///
109    /// Errors if `other` is zero.
110    pub fn __truediv__(&self, other: f64) -> PyResult<Self> {
111        if other == 0.0 {
112            return Err(pyo3::exceptions::PyZeroDivisionError::new_err(
113                "Division by zero",
114            ));
115        }
116        Ok(*self / other)
117    }
118
119    #[must_use]
120    pub fn distance(&self, other: &Self) -> f64 {
121        (*self - *other).length()
122    }
123
124    #[must_use]
125    pub fn distance_2d(&self, other: &Self) -> f64 {
126        (self.x - other.x).hypot(self.y - other.y)
127    }
128
129    #[must_use]
130    pub fn dot(&self, other: &Self) -> f64 {
131        self.z
132            .mul_add(other.z, self.x.mul_add(other.x, self.y * other.y))
133    }
134
135    #[must_use]
136    pub fn cross(&self, other: &Self) -> Self {
137        Self::new(
138            self.y.mul_add(other.z, -(self.z * other.y)),
139            self.z.mul_add(other.x, -(self.x * other.z)),
140            self.x.mul_add(other.y, -(self.y * other.x)),
141        )
142    }
143
144    #[must_use]
145    pub fn length(&self) -> f64 {
146        self.z
147            .mul_add(self.z, self.y.mul_add(self.y, self.x.powi(2)))
148            .sqrt()
149    }
150
151    #[must_use]
152    pub fn normalize(&self) -> Self {
153        let len = self.length();
154        if len == 0.0 {
155            return Self::new(0.0, 0.0, 0.0);
156        }
157        Self::new(self.x / len, self.y / len, self.z / len)
158    }
159
160    /// Check if a jump from self to other is possible
161    #[must_use]
162    pub fn can_jump_to(&self, other: &Self) -> bool {
163        let mut h_distance = self.distance_2d(other);
164        if h_distance <= 0.0 {
165            return true;
166        }
167        // Technically the modification factor to player width should be sqrt(2)
168        // But i have found that it can then make jumps that are just too far
169        // So i have reduced it.
170        let foothold_width_correction = PLAYER_WIDTH * 1.15;
171        h_distance = 0_f64.max(h_distance - (foothold_width_correction));
172
173        // Time to travel the horizontal distance between self and other
174        // with running speed
175        // Or if we are closer than the apex, then take the time to the apex
176        // Equivalent to setting z_at_dest = self.z + JUMP_HEIGHT + CROUCH_JUMP_HEIGHT_GAIN
177        let t = (h_distance / RUNNING_SPEED).max(jump_speed() / GRAVITY);
178
179        // In my jump, at which height am i when i reach the destination x-y distance.
180        let z_at_dest = (0.5 * GRAVITY * t).mul_add(-t, jump_speed().mul_add(t, self.z))
181            + CROUCH_JUMP_HEIGHT_GAIN;
182        // Am i at or above my target height?
183        z_at_dest >= other.z
184    }
185
186    #[allow(clippy::needless_pass_by_value)]
187    fn __iter__(slf: PyRef<'_, Self>) -> PyResult<Py<Iter>> {
188        let iter = Iter {
189            inner: vec![slf.x, slf.y, slf.z].into_iter(),
190        };
191        Py::new(slf.py(), iter)
192    }
193}
194
195#[pyclass]
196struct Iter {
197    inner: std::vec::IntoIter<f64>,
198}
199
200#[pymethods]
201impl Iter {
202    #[allow(clippy::self_named_constructors)]
203    const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
204        slf
205    }
206
207    fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<f64> {
208        slf.inner.next()
209    }
210}
211
212/// Inverse Distance Weighting interpolation of the positions z-values at the target x-y position
213///
214/// <https://en.wikipedia.org/wiki/Inverse_distance_weighting>
215#[must_use]
216pub fn inverse_distance_weighting(points: &[Position], target: (f64, f64)) -> f64 {
217    let p = 2.0; // Power parameter
218    let mut weighted_sum = 0.0;
219    let mut weight_sum = 0.0;
220
221    for &pos in points {
222        let dx = target.0 - pos.x;
223        let dy = target.1 - pos.y;
224        let dist = dx.hypot(dy);
225
226        // Avoid division by zero by setting a small threshold
227        let weight = if dist < 1e-10 {
228            return pos.z; // If target is exactly on a point, return its value
229        } else {
230            1.0 / dist.powf(p)
231        };
232
233        weighted_sum += weight * pos.z;
234        weight_sum += weight;
235    }
236
237    weighted_sum / weight_sum
238}
239
240#[pyfunction]
241#[allow(clippy::needless_pass_by_value)]
242#[pyo3(name = "inverse_distance_weighting")]
243#[must_use]
244pub fn idw_py(points: Vec<Position>, target: (f64, f64)) -> f64 {
245    inverse_distance_weighting(&points, target)
246}