Skip to content

Commit 3a3fab7

Browse files
committed
nav: A* via the pathfinding crate, delete lib/nav
The search itself now runs on pathfinding::prelude::astar with the same fixed-point octile cost model (1000/1414) and the same blocked start/goal guards; the walkability grid and nearest-walkable spiral (genuine map semantics) move into world::core::nav. lib/nav (171 LOC) and its tests are deleted. Equal-cost ties may resolve to a different equally-short waypoint order — path length, duration and endpoints are unchanged. First-party LOC 10911 -> 10844 (-67).
1 parent 71002bc commit 3a3fab7

11 files changed

Lines changed: 169 additions & 224 deletions

File tree

Cargo.lock

Lines changed: 36 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ members = [
44
"lib/rift",
55
"lib/rift/derive",
66
"lib/math",
7-
"lib/nav",
87
"lib/auth",
98
"app/game/world",
109
"app/game/server",

app/game/world/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ include_dir = "0.7"
99
rift = { path = "../../../lib/rift", default-features = false }
1010
math = { path = "../../../lib/math" }
1111
tiled = { version = "0.15", default-features = false }
12-
nav = { path = "../../../lib/nav" }
12+
pathfinding = "4"
1313
serde.workspace = true
1414
serde_json.workspace = true
1515

app/game/world/src/core/area.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use tiled::{LayerType, PropertyValue};
1010
use crate::core::actors::SfxId;
1111
use crate::core::assets;
1212
use crate::core::math::{Pixels, Pos, Rect, Size, Tiles, Tiling};
13+
use crate::core::nav;
1314
use crate::core::table;
1415

1516
const FILE: &str = "area_table.json";
@@ -134,7 +135,7 @@ pub struct Area {
134135
pub width: Tiles,
135136
pub height: Tiles,
136137

137-
pub grid: nav::Grid<Tiles>,
138+
pub grid: nav::Grid,
138139
pub tile_sfx: Vec<Option<SfxId>>,
139140
pub spawn: Pos<Tiles>,
140141
pub portals: Vec<Portal>,
@@ -608,7 +609,7 @@ fn build_grid(
608609
layers: &[RenderLayer],
609610
tiles: &TileTable,
610611
obscuring: &[Rect<Tiles>],
611-
) -> nav::Grid<Tiles> {
612+
) -> nav::Grid {
612613
let (width, height) = (size.x.0 as i32, size.y.0 as i32);
613614
let cells = (width * height) as usize;
614615
let mut any_walkable = vec![false; cells];

app/game/world/src/core/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod area;
33
pub mod assets;
44
pub mod identity;
55
pub mod math;
6+
pub mod nav;
67
pub mod protocol;
78
pub mod session;
89
pub mod table;

app/game/world/src/core/nav.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//! The walkability grid over a map's tiles, and pathfinding across it. A* step costs are
2+
//! fixed-point — one tile = 1000, a diagonal = √2 ≈ 1414 — so search stays deterministic and the
3+
//! octile heuristic is exact.
4+
5+
use crate::core::math::{Pos, Size, Tiles};
6+
7+
const ORTHOGONAL: u32 = 1000;
8+
const DIAGONAL: u32 = 1414;
9+
10+
/// A walkability grid in tile space. A [`Pos<Tiles>`] selects the integer cell it falls in, so
11+
/// callers pass world positions directly without flooring them first.
12+
#[derive(Clone)]
13+
pub struct Grid {
14+
size: Size<Tiles>,
15+
walkable: Vec<bool>,
16+
}
17+
18+
impl Grid {
19+
pub fn new(size: Size<Tiles>, walkable: Vec<bool>) -> Grid {
20+
Grid { size, walkable }
21+
}
22+
23+
pub fn size(&self) -> Size<Tiles> {
24+
self.size
25+
}
26+
27+
pub fn walkable(&self, p: Pos<Tiles>) -> bool {
28+
self.cell_walkable(cell(p))
29+
}
30+
31+
/// The nearest walkable cell's position (its lower corner), spiralling outward; `None` if the
32+
/// whole grid is blocked.
33+
pub fn nearest_walkable(&self, p: Pos<Tiles>) -> Option<Pos<Tiles>> {
34+
let from = cell(p);
35+
if self.cell_walkable(from) {
36+
return Some(at(from));
37+
}
38+
let (width, height) = self.dims();
39+
for radius in 1..=width.max(height) {
40+
let mut best: Option<(i32, i32)> = None;
41+
let mut best_d2 = i64::MAX;
42+
for ny in (from.1 - radius)..=(from.1 + radius) {
43+
for nx in (from.0 - radius)..=(from.0 + radius) {
44+
if (nx - from.0).abs() != radius && (ny - from.1).abs() != radius {
45+
continue;
46+
}
47+
let d2 = i64::from(nx - from.0).pow(2) + i64::from(ny - from.1).pow(2);
48+
if self.cell_walkable((nx, ny)) && d2 < best_d2 {
49+
best_d2 = d2;
50+
best = Some((nx, ny));
51+
}
52+
}
53+
}
54+
if let Some(best) = best {
55+
return Some(at(best));
56+
}
57+
}
58+
None
59+
}
60+
61+
fn dims(&self) -> (i32, i32) {
62+
(self.size.x.0 as i32, self.size.y.0 as i32)
63+
}
64+
65+
fn cell_walkable(&self, c: (i32, i32)) -> bool {
66+
let (width, height) = self.dims();
67+
c.0 >= 0
68+
&& c.1 >= 0
69+
&& c.0 < width
70+
&& c.1 < height
71+
&& self.walkable[(c.1 * width + c.0) as usize]
72+
}
73+
}
74+
75+
/// The cheapest 8-connected path from `start` to `goal` as cell lower-corner positions, both
76+
/// inclusive; `None` when either end is blocked or no route exists.
77+
pub fn astar(grid: &Grid, start: Pos<Tiles>, goal: Pos<Tiles>) -> Option<Vec<Pos<Tiles>>> {
78+
let start = cell(start);
79+
let goal = cell(goal);
80+
if !grid.cell_walkable(start) || !grid.cell_walkable(goal) {
81+
return None;
82+
}
83+
let (path, _cost) = pathfinding::prelude::astar(
84+
&start,
85+
|&(x, y)| {
86+
NEIGHBOURS.iter().filter_map(move |&(dx, dy)| {
87+
let next = (x + dx, y + dy);
88+
let cost = if dx != 0 && dy != 0 {
89+
DIAGONAL
90+
} else {
91+
ORTHOGONAL
92+
};
93+
grid.cell_walkable(next).then_some((next, cost))
94+
})
95+
},
96+
|&(x, y)| {
97+
let dx = (x - goal.0).unsigned_abs();
98+
let dy = (y - goal.1).unsigned_abs();
99+
(dx.max(dy) - dx.min(dy)) * ORTHOGONAL + dx.min(dy) * DIAGONAL
100+
},
101+
|&node| node == goal,
102+
)?;
103+
Some(path.into_iter().map(at).collect())
104+
}
105+
106+
const NEIGHBOURS: [(i32, i32); 8] = [
107+
(1, 0),
108+
(-1, 0),
109+
(0, 1),
110+
(0, -1),
111+
(1, 1),
112+
(1, -1),
113+
(-1, 1),
114+
(-1, -1),
115+
];
116+
117+
/// The integer cell a position falls in.
118+
fn cell(p: Pos<Tiles>) -> (i32, i32) {
119+
(p.x.0.floor() as i32, p.y.0.floor() as i32)
120+
}
121+
122+
/// A cell's lower-corner position.
123+
fn at(c: (i32, i32)) -> Pos<Tiles> {
124+
Pos::new(Tiles(c.0 as f32), Tiles(c.1 as f32))
125+
}

app/game/world/src/features/movement.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ fn route(world: &World, entity: Entity, goal: Pos<Tiles>) -> Option<Vec<Cell>> {
218218
let area = &area::areas()[area_id.0 as usize];
219219
let at = position(world, entity)?;
220220
let goal = area.grid.nearest_walkable(goal)?;
221-
let mut path = nav::astar(&area.grid, at, goal)?;
221+
let mut path = crate::core::nav::astar(&area.grid, at, goal)?;
222222
if path.len() > 1 {
223223
path.remove(0);
224224
}

app/game/world/tests/core.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ fn every_area_is_walkable_pathable_and_connected() {
101101
assert!(
102102
area.portals.iter().any(|portal| {
103103
let tile = portal.rect.center();
104-
area.grid.walkable(tile) && nav::astar(&area.grid, spawn, tile).is_some()
104+
area.grid.walkable(tile)
105+
&& world::core::nav::astar(&area.grid, spawn, tile).is_some()
105106
}),
106107
"{name}: a portal must be reachable from spawn",
107108
);

lib/nav/Cargo.toml

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)