1use gizmo_math::Vec3;
2use std::cmp::Ordering;
3use std::collections::{BinaryHeap, HashMap, HashSet};
4
5#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
6pub struct GridPos {
7 pub x: i32,
8 pub y: i32,
9 pub z: i32, }
11
12impl GridPos {
13 pub fn new(x: i32, y: i32, z: i32) -> Self {
14 Self { x, y, z }
15 }
16}
17
18pub struct NavGrid {
19 pub cell_size: f32,
20 pub width: i32,
21 pub height: i32,
22 pub obstacles: HashSet<GridPos>,
23 pub needs_rebuild: bool,
24}
25
26impl NavGrid {
27 pub fn new(cell_size: f32, width: i32, height: i32) -> Self {
28 Self {
29 cell_size,
30 width,
31 height,
32 obstacles: HashSet::new(),
33 needs_rebuild: true,
34 }
35 }
36
37 pub fn add_obstacle_world(&mut self, world_pos: Vec3) {
38 let gp = self.world_to_grid(world_pos);
39 self.obstacles.insert(gp);
40 }
41
42 pub fn remove_obstacle_world(&mut self, world_pos: Vec3) {
43 let gp = self.world_to_grid(world_pos);
44 self.obstacles.remove(&gp);
45 }
46
47 pub fn world_to_grid(&self, pos: Vec3) -> GridPos {
48 GridPos {
49 x: (pos.x / self.cell_size).floor() as i32,
50 y: (pos.y / self.cell_size).floor() as i32,
51 z: (pos.z / self.cell_size).floor() as i32, }
53 }
54
55 pub fn grid_to_world(&self, gp: GridPos) -> Vec3 {
56 Vec3::new(
57 (gp.x as f32 + 0.5) * self.cell_size,
58 (gp.y as f32 + 0.5) * self.cell_size, (gp.z as f32 + 0.5) * self.cell_size,
60 )
61 }
62
63 pub fn is_walkable(&self, pos: GridPos) -> bool {
64 if pos.x < 0 || pos.x >= self.width || pos.z < 0 || pos.z >= self.height {
65 return false;
66 }
67 !self.obstacles.contains(&pos) }
69
70 pub fn get_neighbors(&self, pos: GridPos) -> Vec<GridPos> {
72 let mut neighbors = Vec::with_capacity(8);
73 let dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)];
74 let diagonals = [(1, 1), (-1, -1), (-1, 1), (1, -1)];
75
76 for (dx, dz) in dirs.iter() {
78 let n = GridPos::new(pos.x + dx, pos.y, pos.z + dz);
79 if self.is_walkable(n) {
80 neighbors.push(n);
81 }
82 }
83
84 for (dx, dz) in diagonals.iter() {
86 let n = GridPos::new(pos.x + dx, pos.y, pos.z + dz);
87 let side1 = GridPos::new(pos.x + dx, pos.y, pos.z);
88 let side2 = GridPos::new(pos.x, pos.y, pos.z + dz);
89
90 if self.is_walkable(n) && self.is_walkable(side1) && self.is_walkable(side2) {
91 neighbors.push(n);
92 }
93 }
94 neighbors
95 }
96
97 pub fn update_from_physics_world(&mut self, physics: &gizmo_physics_rigid::world::PhysicsWorld) {
99 let cell_size = self.cell_size;
100
101 let world_to_grid_fn = |pos: Vec3| -> GridPos {
102 GridPos {
103 x: (pos.x / cell_size).floor() as i32,
104 y: (pos.y / cell_size).floor() as i32,
105 z: (pos.z / cell_size).floor() as i32,
106 }
107 };
108
109 let chunk_size = (physics.entities.len() / 8).max(1);
110
111 self.obstacles = std::thread::scope(|s| {
112 let mut handles = Vec::new();
113
114 let mut start = 0;
115 while start < physics.entities.len() {
116 let end = (start + chunk_size).min(physics.entities.len());
117 handles.push(s.spawn(move || {
118 let mut local_obs = HashSet::new();
119 for i in start..end {
120 let rb = &physics.rigid_bodies[i];
121 if rb.body_type == gizmo_physics_rigid::components::BodyType::Dynamic {
122 continue;
123 }
124 let transform = &physics.transforms[i];
125 let collider = &physics.colliders[i];
126
127 let aabb = collider.compute_aabb(transform.position, transform.rotation);
128 let min_grid = world_to_grid_fn(aabb.min.into());
129 let max_grid = world_to_grid_fn(aabb.max.into());
130
131 for x in min_grid.x..=max_grid.x {
132 for y in min_grid.y..=max_grid.y {
133 for z in min_grid.z..=max_grid.z {
134 local_obs.insert(GridPos::new(x, y, z));
135 }
136 }
137 }
138 }
139 local_obs
140 }));
141 start = end;
142 }
143
144 let mut combined = HashSet::new();
145 for handle in handles {
146 combined.extend(handle.join().unwrap());
147 }
148 combined
149 });
150
151 self.needs_rebuild = false;
152 }
153}
154
155#[derive(Copy, Clone, PartialEq)]
156struct AStarNode {
157 pos: GridPos,
158 cost: u32, }
160
161impl Eq for AStarNode {}
162
163impl Ord for AStarNode {
165 fn cmp(&self, other: &Self) -> Ordering {
166 other
167 .cost
168 .cmp(&self.cost) .then_with(|| self.pos.x.cmp(&other.pos.x))
170 .then_with(|| self.pos.y.cmp(&other.pos.y))
171 .then_with(|| self.pos.z.cmp(&other.pos.z))
172 }
173}
174
175impl PartialOrd for AStarNode {
176 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
177 Some(self.cmp(other))
178 }
179}
180
181fn heuristic(a: GridPos, b: GridPos) -> u32 {
183 let dx = (a.x - b.x).unsigned_abs();
184 let dz = (a.z - b.z).unsigned_abs();
185 let (mn, mx) = if dx < dz { (dx, dz) } else { (dz, dx) };
186 14 * mn + 10 * (mx - mn)
187}
188
189impl NavGrid {
190 pub fn find_path(&self, start_world: Vec3, end_world: Vec3) -> Option<Vec<Vec3>> {
192 let start = self.world_to_grid(start_world);
193 let end = self.world_to_grid(end_world);
194
195 if !self.is_walkable(end) || !self.is_walkable(start) {
196 return None; }
198
199 let mut open_set = BinaryHeap::new();
200 let mut came_from: HashMap<GridPos, GridPos> = HashMap::new();
201 let mut g_score: HashMap<GridPos, u32> = HashMap::new();
202 let mut closed_set: HashSet<GridPos> = HashSet::new();
203
204 open_set.push(AStarNode {
205 pos: start,
206 cost: 0,
207 });
208 g_score.insert(start, 0);
209
210 let max_iterations = 25_000usize;
211
212 let mut iterations = 0usize;
213 while let Some(current_node) = open_set.pop() {
214 iterations += 1;
215 if iterations > max_iterations {
216 tracing::error!(
217 "[AI] Pathfinding limit aşıldı ({}/{}). Ulaşılamaz rota?",
218 iterations, max_iterations
219 );
220 break;
221 }
222
223 let current = current_node.pos;
224
225 if closed_set.contains(¤t) {
226 continue;
227 }
228 closed_set.insert(current);
229
230 if current == end {
231 let mut path = Vec::new();
233 let mut curr = end;
234 while curr != start {
235 path.push(self.grid_to_world(curr));
236 curr = match came_from.get(&curr) {
237 Some(p) => *p,
238 None => break,
239 };
240 }
241 path.reverse();
242 return Some(path);
243 }
244
245 let curr_g = *g_score.get(¤t).unwrap_or(&u32::MAX);
246
247 for neighbor in self.get_neighbors(current) {
248 let move_cost = if neighbor.x != current.x && neighbor.z != current.z {
250 14
251 } else {
252 10
253 };
254 let tentative_g = curr_g + move_cost;
255
256 if tentative_g < *g_score.get(&neighbor).unwrap_or(&u32::MAX) {
257 came_from.insert(neighbor, current);
258 g_score.insert(neighbor, tentative_g);
259
260 let f_score = tentative_g + heuristic(neighbor, end);
261 open_set.push(AStarNode {
262 pos: neighbor,
263 cost: f_score,
264 });
265 }
266 }
267 }
268
269 None }
271}