Level partial class methods for building and debugging an A* navigation Grid for a level. GenerateGrid builds a Grid using scene geometry with filtering, logs diagnostics, assigns edge bands, and marks cells under doors; DrawGrid is a console command that logs info and draws cells for debugging.
using System.Linq;
using System.Threading.Tasks;
using Sandbox;
using GridAStar;
namespace BrickJam;
public abstract partial class Level
{
/// <summary>The A* navigation grid for this level (host-side). Built in <see cref="GenerateGrid"/>.</summary>
public Grid Grid { get; private set; }
/// <summary>
/// Build the grid conforming to this level's <see cref="WorldBox"/>. Scene-System port of the legacy
/// <c>Level.GenerateGrid</c>. Single background thread (user choice). Include-tags are cleared because the
/// runtime-loaded map's world collision isn't reliably tagged "solid"; we instead generate on all static
/// geometry minus the actor tags.
/// </summary>
public async Task GenerateGrid()
{
Grid?.Delete();
var scene = MansionGame.Instance.Scene;
var builder = new GridBuilder( Type.ToString() )
.WithBounds( Vector3.Zero, WorldBox, Rotation.Identity )
.WithStaticOnly( false )
// Generate on the world floor. The map's world collision mesh is tagged "world"; some solid props
// use "solid". WithGridSettings matches ANY include tag, so we conform to the floor and ignore
// clutter/furniture (which would otherwise block cells).
.WithTags( "world" )
.WithoutTags( "player", "npc", "door", "loot", "nocollide" )
.WithCellSize( 12f )
.WithHeightClearance( 72f )
// Tighter horizontal clearance so cells GENERATE and CONNECT through narrow doorways (12 needed a
// ~24u+ opening; tight doorframes fell just under it, leaving gaps NPCs couldn't path across). NPC
// capsule radii were lowered to match (NPC=9, Doob=8) so they physically fit where cells now exist.
.WithWidthClearance( 8f )
.WithStepSize( 24f )
.WithStandableAngle( 70f )
.WithMaxDropHeight( 200f );
// Generation diagnostics: reset, then log the reject breakdown after.
GridAStar.Grid.DebugCastsHit = GridAStar.Grid.DebugAngleRejected = GridAStar.Grid.DebugOutOfBounds = 0;
Cell.DebugCreated = Cell.DebugRejectCoords = Cell.DebugRejectClearance = 0;
Grid = await builder.Create( scene, threadedChunkSides: 1, printInfo: true );
Log.Info( $"[grid] castsHit={GridAStar.Grid.DebugCastsHit} outOfBounds={GridAStar.Grid.DebugOutOfBounds} angleRejected={GridAStar.Grid.DebugAngleRejected} " +
$"-> TryCreate: created={Cell.DebugCreated} rejectCoords={Cell.DebugRejectCoords} rejectClearance={Cell.DebugRejectClearance}" );
// Outer/inner edge bands so pathing prefers staying away from ledges (legacy parity).
await Grid.AssignEdgeCells( tagToExclude: "edge", tagToAssign: "outeredge" );
await Grid.AssignEdgeCells( tagToExclude: "outeredge", tagToAssign: "inneredge" );
// Mark the cells under closed doors as door cells so NPCs route around them until opened.
foreach ( var door in scene.GetAllComponents<Door>() )
if ( Grid.IsInsideBounds( door.WorldPosition ) )
door.OccupyCells();
}
/// <summary>
/// Debug: draw the current level's grid cells (cyan) for 15s and log the grid bounds + the local player's
/// position, so we can see whether the grid actually covers the playable floor.
/// </summary>
[ConCmd( "grid_draw" )]
public static void DrawGrid()
{
var grid = MansionGame.Instance?.CurrentLevel?.Grid;
var pawn = Player.Local;
Log.Info( $"[grid] player at {pawn?.WorldPosition}" );
if ( grid is null )
{
Log.Info( "[grid] no grid on the current level" );
return;
}
Log.Info( $"[grid] bounds {grid.WorldBounds} | cells {grid.AllCells.Count()}" );
foreach ( var cell in grid.AllCells )
cell.Draw( Color.Cyan, 15f, depthTest: false );
}
}