@@ -61,15 +61,15 @@ def _validate_endpoints(
6161
6262 snap_radius = max (10 , int (100.0 / terrain .cell_size ))
6363 if not _cell_walkable (terrain , sc , sr , near_z ):
64- snapped = _snap_to_walkable (terrain , sc , sr , radius = snap_radius )
64+ snapped = _snap_to_walkable (terrain , sc , sr , radius = snap_radius , near_z = near_z )
6565 if snapped :
6666 sc , sr = snapped
6767 else :
6868 log .warning ("[NAV] Pathfind: start is blocked and no walkable cell nearby" )
6969 return None
7070
7171 if not _cell_walkable (terrain , gc , gr , near_z ):
72- snapped = _snap_to_walkable (terrain , gc , gr , radius = snap_radius )
72+ snapped = _snap_to_walkable (terrain , gc , gr , radius = snap_radius , near_z = near_z )
7373 if snapped :
7474 gc , gr = snapped
7575 else :
@@ -199,11 +199,15 @@ def find_path(
199199 rows = terrain ._rows
200200 explored = 0
201201
202- # Extract bitfield for hot-path (local refs = faster than attr access)
203- if not terrain ._walk_bits :
204- terrain ._build_walk_bits ()
205- wb = terrain ._walk_bits
206- wbc = terrain ._walk_byte_cols
202+ # Build a Z-filtered walkability bitfield so the search never routes
203+ # through cells on a different vertical level (bridges, multi-floor).
204+ if not math .isnan (near_z ) and terrain ._z_ceiling :
205+ wb , wbc = terrain .build_walk_bits_z (near_z )
206+ else :
207+ if not terrain ._walk_bits :
208+ terrain ._build_walk_bits ()
209+ wb = terrain ._walk_bits
210+ wbc = terrain ._walk_byte_cols
207211
208212 # All 8 directions for the start node (no parent to prune from)
209213 _ALL_DIRS = [
@@ -297,8 +301,9 @@ def _cell_walkable(terrain: ZoneTerrain, col: int, row: int, near_z: float = flo
297301 """Check if a grid cell is walkable with Z-level filtering.
298302
299303 When near_z is provided (not NaN), cells on a different vertical
300- level (bridges, multi-floor buildings) are rejected. This prevents
301- pathfinding from routing through geometry on the wrong floor.
304+ level (bridges, multi-floor buildings) are rejected. Bridge cells
305+ with water/lava underneath are only walkable for agents at bridge
306+ height, not ground level.
302307 """
303308 idx = terrain ._grid_idx (col , row )
304309 if idx < 0 :
@@ -310,6 +315,18 @@ def _cell_walkable(terrain: ZoneTerrain, col: int, row: int, near_z: float = flo
310315 if not terrain .is_walkable (game_x , game_y ):
311316 return False
312317 if not math .isnan (near_z ):
318+ # Multi-level check: reject bridge cells when agent is at ground
319+ # level and ground surface is hazardous (water/lava).
320+ from nav .terrain .heightmap import SURFACE_BRIDGE , SURFACE_LAVA , SURFACE_WATER
321+
322+ f = terrain ._flags [idx ]
323+ z_ceil = terrain .get_z_ceiling (game_x , game_y )
324+ if not math .isnan (z_ceil ):
325+ z_ground = terrain .get_z (game_x , game_y )
326+ mid = (z_ground + z_ceil ) / 2.0
327+ on_upper = near_z > mid
328+ if not on_upper and (f & SURFACE_BRIDGE ) and (f & (SURFACE_WATER | SURFACE_LAVA )):
329+ return False
313330 level_z = terrain .get_level_z (game_x , game_y , near_z )
314331 if abs (level_z - near_z ) > _WALKABLE_CLIMB :
315332 return False
@@ -352,14 +369,16 @@ def _cell_cost(terrain: ZoneTerrain, col: int, row: int, from_col: int = -1, fro
352369 return cost
353370
354371
355- def _snap_to_walkable (terrain : ZoneTerrain , col : int , row : int , radius : int = 5 ) -> tuple [int , int ] | None :
356- """Find nearest walkable cell within radius."""
372+ def _snap_to_walkable (
373+ terrain : ZoneTerrain , col : int , row : int , radius : int = 5 , near_z : float = float ("nan" )
374+ ) -> tuple [int , int ] | None :
375+ """Find nearest walkable cell within radius, respecting Z-level."""
357376 best = None
358377 best_dist = float ("inf" )
359378 for dr in range (- radius , radius + 1 ):
360379 for dc in range (- radius , radius + 1 ):
361380 nc , nr = col + dc , row + dr
362- if _cell_walkable (terrain , nc , nr ):
381+ if _cell_walkable (terrain , nc , nr , near_z ):
363382 d = dc * dc + dr * dr
364383 if d < best_dist :
365384 best_dist = d
@@ -399,13 +418,13 @@ def _find_path_astar(
399418
400419 snap_radius = max (10 , int (100.0 / terrain .cell_size ))
401420 if not _bit_walkable (wb , wbc , sc , sr , cols , rows ):
402- snapped = _snap_to_walkable (terrain , sc , sr , radius = snap_radius )
421+ snapped = _snap_to_walkable (terrain , sc , sr , radius = snap_radius , near_z = near_z )
403422 if snapped :
404423 sc , sr = snapped
405424 else :
406425 return None
407426 if not _bit_walkable (wb , wbc , gc , gr , cols , rows ):
408- snapped = _snap_to_walkable (terrain , gc , gr , radius = snap_radius )
427+ snapped = _snap_to_walkable (terrain , gc , gr , radius = snap_radius , near_z = near_z )
409428 if snapped :
410429 gc , gr = snapped
411430 else :
0 commit comments