Levels/Level.Grid.cs

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.

File Access
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 );
	}
}