Skip to main content

flatland_pathfinding/
z_nav.rs

1//! Z-level helpers for outdoor pathfinding.
2
3use flatland_protocol::ZTransitionView;
4
5use crate::grid::NavWorld;
6
7pub const Z_LEVEL_TOLERANCE: f32 = 0.35;
8
9pub fn walkable_levels_at(world: &NavWorld, x: f32, y: f32) -> Vec<f32> {
10    let mut levels = vec![world.elevation_at(x, y)];
11    for p in &world.z_platforms {
12        if x >= p.x0 && x <= p.x1 && y >= p.y0 && y <= p.y1 {
13            levels.push(p.z);
14        }
15    }
16    for tr in &world.z_transitions {
17        if x >= tr.x0 && x <= tr.x1 && y >= tr.y0 && y <= tr.y1 {
18            levels.push(tr.z_from);
19            levels.push(tr.z_to);
20        }
21    }
22    levels.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
23    levels.dedup_by(|a, b| (*a - *b).abs() < Z_LEVEL_TOLERANCE);
24    levels
25}
26
27pub fn is_walkable_at_z(world: &NavWorld, x: f32, y: f32, z: f32) -> bool {
28    walkable_levels_at(world, x, y)
29        .iter()
30        .any(|&l| (l - z).abs() <= Z_LEVEL_TOLERANCE)
31}
32
33fn nearest_level(levels: &[f32], current: f32) -> f32 {
34    levels
35        .iter()
36        .min_by(|a, b| {
37            (*a - current)
38                .abs()
39                .partial_cmp(&(*b - current).abs())
40                .unwrap_or(std::cmp::Ordering::Equal)
41        })
42        .copied()
43        .unwrap_or(current)
44}
45
46pub fn goal_z_for(world: &NavWorld, goal_x: f32, goal_y: f32, player_z: f32) -> f32 {
47    nearest_level(&walkable_levels_at(world, goal_x, goal_y), player_z)
48}
49
50fn transition_at<'a>(world: &'a NavWorld, x: f32, y: f32) -> Option<&'a ZTransitionView> {
51    world
52        .z_transitions
53        .iter()
54        .find(|t| x >= t.x0 && x <= t.x1 && y >= t.y0 && y <= t.y1)
55}
56
57trait ZTransitionExt {
58    fn z_min(&self) -> f32;
59    fn z_max(&self) -> f32;
60}
61
62impl ZTransitionExt for ZTransitionView {
63    fn z_min(&self) -> f32 {
64        self.z_from.min(self.z_to)
65    }
66
67    fn z_max(&self) -> f32 {
68        self.z_from.max(self.z_to)
69    }
70}
71
72pub fn cell_walkable_for_path(
73    world: &NavWorld,
74    x: f32,
75    y: f32,
76    player_z: f32,
77    goal_z: f32,
78) -> bool {
79    if is_walkable_at_z(world, x, y, player_z) {
80        return true;
81    }
82    if let Some(tr) = transition_at(world, x, y) {
83        let on_from = (player_z - tr.z_from).abs() <= Z_LEVEL_TOLERANCE;
84        let on_to = (player_z - tr.z_to).abs() <= Z_LEVEL_TOLERANCE;
85        let goal_on_from = (goal_z - tr.z_from).abs() <= Z_LEVEL_TOLERANCE;
86        let goal_on_to = (goal_z - tr.z_to).abs() <= Z_LEVEL_TOLERANCE;
87        if (on_from && goal_on_to) || (on_to && goal_on_from) {
88            return true;
89        }
90    }
91    false
92}
93
94/// Vertical intent while auto-walking (stairs).
95pub fn transition_vertical(world: &NavWorld, px: f32, py: f32, pz: f32, goal_z: f32) -> f32 {
96    let Some(tr) = transition_at(world, px, py) else {
97        return 0.0;
98    };
99    if (pz - goal_z).abs() <= Z_LEVEL_TOLERANCE {
100        return 0.0;
101    }
102    if goal_z > pz + Z_LEVEL_TOLERANCE
103        && (pz - tr.z_from).abs() <= Z_LEVEL_TOLERANCE
104        && tr.z_to > tr.z_from
105    {
106        return 1.0;
107    }
108    if goal_z < pz - Z_LEVEL_TOLERANCE
109        && (pz - tr.z_to).abs() <= Z_LEVEL_TOLERANCE
110        && tr.z_to > tr.z_from
111    {
112        return -1.0;
113    }
114    if goal_z > pz + Z_LEVEL_TOLERANCE && pz <= tr.z_min() + Z_LEVEL_TOLERANCE {
115        return 1.0;
116    }
117    if goal_z < pz - Z_LEVEL_TOLERANCE && pz >= tr.z_max() - Z_LEVEL_TOLERANCE {
118        return -1.0;
119    }
120    0.0
121}