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 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 #[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 let foothold_width_correction = PLAYER_WIDTH * 1.15;
171 h_distance = 0_f64.max(h_distance - (foothold_width_correction));
172
173 let t = (h_distance / RUNNING_SPEED).max(jump_speed() / GRAVITY);
178
179 let z_at_dest = (0.5 * GRAVITY * t).mul_add(-t, jump_speed().mul_add(t, self.z))
181 + CROUCH_JUMP_HEIGHT_GAIN;
182 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#[must_use]
216pub fn inverse_distance_weighting(points: &[Position], target: (f64, f64)) -> f64 {
217 let p = 2.0; 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 let weight = if dist < 1e-10 {
228 return pos.z; } 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}