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 #[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 let foothold_width_correction = PLAYER_WIDTH * 1.15;
135 h_distance = 0_f64.max(h_distance - (foothold_width_correction));
136
137 let t = (h_distance / RUNNING_SPEED).max(jump_speed() / GRAVITY);
142
143 let z_at_dest = (0.5 * GRAVITY * t).mul_add(-t, jump_speed().mul_add(t, self.z))
145 + CROUCH_JUMP_HEIGHT_GAIN;
146 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#[must_use]
180pub fn inverse_distance_weighting(points: &[Position], target: (f64, f64)) -> f64 {
181 let p = 2.0; 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 let weight = if dist < 1e-10 {
192 return pos.z; } 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}