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, 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
42#[derive(FromPyObject)]
43enum PositionFromInputOptions {
44    #[pyo3(transparent)]
45    Other(Vec<f64>),
46    #[pyo3(transparent)]
47    Position(Position),
48}
49
50#[pymethods]
51impl Position {
52    #[must_use]
53    #[new]
54    pub const fn new(x: f64, y: f64, z: f64) -> Self {
55        Self { x, y, z }
56    }
57
58    #[staticmethod]
59    fn from_input(value: PositionFromInputOptions) -> PyResult<Self> {
60        match value {
61            PositionFromInputOptions::Position(pos) => Ok(pos),
62            PositionFromInputOptions::Other(input) => {
63                if input.len() != 3 {
64                    return Err(pyo3::exceptions::PyValueError::new_err(
65                        "Input must be a Vector3 or tuple or list of length 3",
66                    ));
67                }
68                Ok(Self::new(input[0], input[1], input[2]))
69            }
70        }
71    }
72
73    #[must_use]
74    pub fn __sub__(&self, other: &Self) -> Self {
75        *self - *other
76    }
77
78    #[must_use]
79    pub fn __add__(&self, other: &Self) -> Self {
80        *self + *other
81    }
82
83    #[must_use]
84    pub fn distance(&self, other: &Self) -> f64 {
85        (*self - *other).length()
86    }
87
88    #[must_use]
89    pub fn distance_2d(&self, other: &Self) -> f64 {
90        (self.x - other.x).hypot(self.y - other.y)
91    }
92
93    #[must_use]
94    pub fn dot(&self, other: &Self) -> f64 {
95        self.z
96            .mul_add(other.z, self.x.mul_add(other.x, self.y * other.y))
97    }
98
99    #[must_use]
100    pub fn cross(&self, other: &Self) -> Self {
101        Self::new(
102            self.y.mul_add(other.z, -(self.z * other.y)),
103            self.z.mul_add(other.x, -(self.x * other.z)),
104            self.x.mul_add(other.y, -(self.y * other.x)),
105        )
106    }
107
108    #[must_use]
109    pub fn length(&self) -> f64 {
110        self.z
111            .mul_add(self.z, self.y.mul_add(self.y, self.x.powi(2)))
112            .sqrt()
113    }
114
115    #[must_use]
116    pub fn normalize(&self) -> Self {
117        let len = self.length();
118        if len == 0.0 {
119            return Self::new(0.0, 0.0, 0.0);
120        }
121        Self::new(self.x / len, self.y / len, self.z / len)
122    }
123
124    /// Check if a jump from self to other is possible
125    #[must_use]
126    pub fn can_jump_to(&self, other: &Self) -> bool {
127        let mut h_distance = self.distance_2d(other);
128        if h_distance <= 0.0 {
129            return true;
130        }
131        // Technically the modification factor to player width should be sqrt(2)
132        // But i have found that it can then make jumps that are just too far
133        // So i have reduced it.
134        let foothold_width_correction = PLAYER_WIDTH * 1.15;
135        h_distance = 0_f64.max(h_distance - (foothold_width_correction));
136
137        // Time to travel the horizontal distance between self and other
138        // with running speed
139        // Or if we are closer than the apex, then take the time to the apex
140        // Equivalent to setting z_at_dest = self.z + JUMP_HEIGHT + CROUCH_JUMP_HEIGHT_GAIN
141        let t = (h_distance / RUNNING_SPEED).max(jump_speed() / GRAVITY);
142
143        // In my jump, at which height am i when i reach the destination x-y distance.
144        let z_at_dest = (0.5 * GRAVITY * t).mul_add(-t, jump_speed().mul_add(t, self.z))
145            + CROUCH_JUMP_HEIGHT_GAIN;
146        // Am i at or above my target height?
147        z_at_dest >= other.z
148    }
149
150    #[allow(clippy::needless_pass_by_value)]
151    fn __iter__(slf: PyRef<'_, Self>) -> PyResult<Py<Iter>> {
152        let iter = Iter {
153            inner: vec![slf.x, slf.y, slf.z].into_iter(),
154        };
155        Py::new(slf.py(), iter)
156    }
157}
158
159#[pyclass]
160struct Iter {
161    inner: std::vec::IntoIter<f64>,
162}
163
164#[pymethods]
165impl Iter {
166    #[allow(clippy::self_named_constructors)]
167    const fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
168        slf
169    }
170
171    fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<f64> {
172        slf.inner.next()
173    }
174}
175
176/// Inverse Distance Weighting interpolation of the positions z-values at the target x-y position
177///
178/// <https://en.wikipedia.org/wiki/Inverse_distance_weighting>
179#[must_use]
180pub fn inverse_distance_weighting(points: &[Position], target: (f64, f64)) -> f64 {
181    let p = 2.0; // Power parameter
182    let mut weighted_sum = 0.0;
183    let mut weight_sum = 0.0;
184
185    for &pos in points {
186        let dx = target.0 - pos.x;
187        let dy = target.1 - pos.y;
188        let dist = dx.hypot(dy);
189
190        // Avoid division by zero by setting a small threshold
191        let weight = if dist < 1e-10 {
192            return pos.z; // If target is exactly on a point, return its value
193        } else {
194            1.0 / dist.powf(p)
195        };
196
197        weighted_sum += weight * pos.z;
198        weight_sum += weight;
199    }
200
201    weighted_sum / weight_sum
202}
203
204#[pyfunction]
205#[allow(clippy::needless_pass_by_value)]
206#[pyo3(name = "inverse_distance_weighting")]
207#[must_use]
208pub fn idw_py(points: Vec<Position>, target: (f64, f64)) -> f64 {
209    inverse_distance_weighting(&points, target)
210}