🔍 s&box Package Code Search

Search C# source code, UI razor templates, shaders, and configs across s&box packages.

Showing code results for query: * (154 total matches found)
rue.house / Game/MansionGame.Music.cs
Game game
using Sandbox;

namespace BrickJam;

public sealed partial class MansionGame
{
	/// <summary>How fast the music fades in/out (per second). Legacy faded on the server tick.</summary>
	public float MusicVolumeChangeRate => 0.5f;

	/// <summary>Target music volume - background level, the tracks are mastered loud.</summary>
	public float MusicVolume => 0.15f;

	private SoundHandle musicHandle;
	private LevelType musicLevel = LevelType.None;
	private float musicVolume;

	/// <summary>
	/// Client-side music orchestration (every client, host included - music is local audio). Scene-System
	/// port of the legacy host-side <c>ProcessMusic</c>: drive the track from the replicated
	/// <see cref="CurrentLevelType"/> and crossfade when the level changes.
	/// </summary>
	protected override void OnUpdate()
	{
		var track = Level.GetMusic( CurrentLevelType );

		if ( CurrentLevelType != musicLevel )
		{
			// Level changed: fade the old track out, then swap once it's silent.
			musicVolume = musicVolume.LerpTo( 0f, MusicVolumeChangeRate * Time.Delta );

			if ( musicVolume <= 0.01f )
			{
				musicHandle?.Stop();
				musicHandle = null;
				musicLevel = CurrentLevelType;
				musicVolume = 0f;
			}

			ApplyMusicVolume();
			return;
		}

		// Same level: keep the track playing (restart if the asset isn't looped) and fade toward target.
		if ( !string.IsNullOrEmpty( track ) )
		{
			if ( musicHandle is null || musicHandle.IsStopped )
			{
				musicHandle = Sound.Play( track );
				if ( musicHandle is not null )
					musicHandle.Volume = musicVolume; // start at the current (faded) level, not full blast
			}

			musicVolume = musicVolume.LerpTo( MusicVolume, MusicVolumeChangeRate * Time.Delta );
		}
		else
		{
			musicVolume = musicVolume.LerpTo( 0f, MusicVolumeChangeRate * Time.Delta );
			if ( musicVolume <= 0.01f && musicHandle is not null )
			{
				musicHandle.Stop();
				musicHandle = null;
			}
		}

		ApplyMusicVolume();
	}

	private void ApplyMusicVolume()
	{
		if ( musicHandle is not null && !musicHandle.IsStopped )
			musicHandle.Volume = musicVolume;
	}
}
rue.house / Grid/AStarPathBuilder.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Sandbox;

namespace GridAStar;

public struct AStarPathBuilder
{
	public Grid Grid { get; private set; } = null;
	public List<string> TagsToExclude { get; private set; } = new() { "occupied" };
	public bool HasTagsToExlude => TagsToExclude.Count() > 0;
	public bool HasOccupiedTagToExclude => HasTagsToExlude ? TagsToExclude.Contains( "occupied" ) : false;
	public List<string> TagsToInclude { get; private set; } = new();
	public bool HasTagsToInclude => TagsToInclude.Count() > 0;
	public Dictionary<string, float> TagsToAvoid { get; private set; } = new();
	public bool HasTagsToAvoid => TagsToAvoid.Count() > 0;
	public bool AcceptsPartial { get; private set; } = false;
	public float MaxCheckDistance { get; private set; } = float.PositiveInfinity;
	public float MaxDropHeight { get; private set; } = GridSettings.DEFAULT_DROP_HEIGHT;
	public Component PathCreator { get; private set; } = null;
	public bool HasPathCreator => PathCreator != null;

	public AStarPathBuilder() { }
	public AStarPathBuilder( Grid grid ) : this()
	{
		Grid = grid;
	}

	public static AStarPathBuilder From( Grid grid ) => new AStarPathBuilder( grid );

	public AStarPathBuilder WithTags( params string[] tags )
	{
		foreach ( var tag in tags )
		{
			if ( !TagsToInclude.Contains( tag ) )
				TagsToInclude.Add( tag );
			if ( TagsToExclude.Contains( tag ) )
				TagsToExclude.Remove( tag );
		}
		return this;
	}

	public AStarPathBuilder WithoutTags( params string[] tags )
	{
		foreach ( var tag in tags )
		{
			if ( !TagsToExclude.Contains( tag ) )
				TagsToExclude.Add( tag );
			if ( TagsToInclude.Contains( tag ) )
				TagsToInclude.Remove( tag );
		}
		return this;
	}

	/// <summary>
	/// Which tags to avoid, when found it will add the malus to its total cost.
	/// </summary>
	public AStarPathBuilder AvoidTag( string tag, float malus )
	{
		malus = Math.Abs( malus );

		if ( !TagsToAvoid.ContainsKey( tag ) )
			TagsToAvoid.Add( tag, malus );
		else
			TagsToAvoid[tag] = malus;

		return this;
	}

	public AStarPathBuilder WithMaxDistance( float maxDistance )
	{
		MaxCheckDistance = Math.Max( 0f, maxDistance );
		return this;
	}

	public AStarPathBuilder WithMaxDropHeight( float maxDropHeight )
	{
		MaxDropHeight = Math.Min( Grid.MaxDropHeight, maxDropHeight );
		return this;
	}

	public AStarPathBuilder WithPartialEnabled()
	{
		AcceptsPartial = true;
		return this;
	}

	public AStarPathBuilder WithPathCreator( Component pathCreator )
	{
		PathCreator = pathCreator;
		return this;
	}

	public AStarPath Run( Cell startingCell, Cell targetCell, bool reversed = false, bool withCellConnections = true )
	{
		if ( startingCell is null || targetCell is null || startingCell == targetCell ) return AStarPath.Empty();

		return AStarPath.From( this, Grid.ComputePathInternal( this, startingCell, targetCell, CancellationToken.None, reversed, withCellConnections ) );
	}
	public AStarPath Run( Vector3 startingPosition, Cell targetCell, bool reversed = false, bool withCellConnections = true ) => Run( Grid.GetCell( startingPosition ), targetCell, reversed, withCellConnections );
	public AStarPath Run( Cell startingCell, Vector3 targetPosition, bool reversed = false, bool withCellConnections = true ) => Run( startingCell, Grid.GetCell( targetPosition ), reversed, withCellConnections );
	public AStarPath Run( Vector3 startingPosition, Vector3 targetPosition, bool reversed = false, bool withCellConnections = true ) => Run( Grid.GetCell( startingPosition ), Grid.GetCell( targetPosition ), reversed, withCellConnections );

	internal AStarPath Run( Cell startingCell, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true )
	{
		if ( startingCell is null || targetCell is null || startingCell == targetCell ) return AStarPath.Empty();

		return AStarPath.From( this, Grid.ComputePathInternal( this, startingCell, targetCell, token, reversed, withCellConnections ) );
	}

	public async Task<AStarPath> RunAsync( Cell startingCell, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true )
	{
		var builder = this;

		return await GameTask.RunInThreadAsync( () => builder.Run( startingCell, targetCell, token, reversed, withCellConnections ) );
	}
	public async Task<AStarPath> RunAsync( Vector3 startingPosition, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true ) => await RunAsync( Grid.GetCell( startingPosition ), targetCell, token, reversed, withCellConnections );
	public async Task<AStarPath> RunAsync( Cell startingCell, Vector3 targetPosition, CancellationToken token, bool reversed = false, bool withCellConnections = true ) => await RunAsync( startingCell, Grid.GetCell( targetPosition ), token, reversed, withCellConnections );
	public async Task<AStarPath> RunAsync( Vector3 startingPosition, Vector3 targetPosition, CancellationToken token, bool reversed = false, bool withCellConnections = true ) => await RunAsync( Grid.GetCell( startingPosition ), Grid.GetCell( targetPosition ), token, reversed, withCellConnections );

	public async Task<AStarPath> RunInParallel( Cell startingCell, Cell targetCell, CancellationTokenSource tokenSource, bool withCellConnections = true )
	{
		if ( startingCell is null || targetCell is null || startingCell == targetCell ) return AStarPath.Empty();

		var fromTo = RunAsync( startingCell, targetCell, tokenSource.Token, false, withCellConnections );
		var toFrom = RunAsync( targetCell, startingCell, tokenSource.Token, true, false ); // You can't reverse some cell connections, like dropping down

		var pathResult = await GameTask.WhenAny( fromTo, toFrom ).Result;

		// Cancel the other task that hasn't finished yet.
		tokenSource.Cancel();

		return pathResult;
	}
	public async Task<AStarPath> RunInParallel( Vector3 startingPosition, Cell targetCell, CancellationTokenSource tokenSource, bool withCellConnections = true ) => await RunInParallel( Grid.GetCell( startingPosition ), targetCell, tokenSource, withCellConnections );
	public async Task<AStarPath> RunInParallel( Cell startingCell, Vector3 targetPosition, CancellationTokenSource tokenSource, bool withCellConnections = true ) => await RunInParallel( startingCell, Grid.GetCell( targetPosition ), tokenSource, withCellConnections );
	public async Task<AStarPath> RunInParallel( Vector3 startingPosition, Vector3 targetPosition, CancellationTokenSource tokenSource, bool withCellConnections = true ) => await RunInParallel( Grid.GetCell( startingPosition ), Grid.GetCell( targetPosition ), tokenSource, withCellConnections );
}
rue.house / Grid/IntVector2.cs
Game game
using System;
using Sandbox;

namespace GridAStar;

/// <summary>
/// Like a Vector2, but with integers instead.
/// </summary>
public struct IntVector2 : IEquatable<IntVector2>
{
	public int x { get; set; }
	public int y { get; set; }

	public int this[int index]
	{
		get
		{
			int result = index switch
			{
				0 => x,
				1 => y,
				_ => throw new IndexOutOfRangeException(),
			};

			return result;
		}
		set
		{
			switch ( index )
			{
				case 0:
					x = value;
					break;
				case 1:
					y = value;
					break;
			}
		}
	}

	public IntVector2( int x, int y )
	{
		this.x = x;
		this.y = y;
	}

	public IntVector2 WithX( int x ) => new IntVector2( x, this.y );
	public IntVector2 WithY( int y ) => new IntVector2( this.x, y );
	public Vector2 ToVector2() => new Vector2( x, y );
	public float DistanceSquared( IntVector2 other ) => ToVector2().DistanceSquared( other.ToVector2() );
	public override string ToString() => $"{x},{y}";
	public override bool Equals( object obj ) => obj is IntVector2 other && Equals( other );
	public bool Equals( IntVector2 other ) => x == other.x && y == other.y;
	public override int GetHashCode() => HashCode.Combine( x, y );

	public static bool operator ==( IntVector2 left, IntVector2 right ) => left.Equals( right );
	public static bool operator !=( IntVector2 left, IntVector2 right ) => !(left == right);
	public static IntVector2 operator +( IntVector2 a, IntVector2 b ) => new IntVector2( a.x + b.x, a.y + b.y );
	public static IntVector2 operator -( IntVector2 a, IntVector2 b ) => new IntVector2( a.x - b.x, a.y - b.y );
}
rue.house / Levels/Level.cs
Game game
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Sandbox;

namespace BrickJam;

/// <summary>
/// Level orchestration. The legacy <c>Level</c> was an <c>Entity</c> (for replication); in the Scene
/// System it is a plain server-side object owned by <see cref="MansionGame"/>. The single replicated
/// fact clients need — the current level type — lives on the manager as
/// <see cref="MansionGame.CurrentLevelType"/>.
///
/// DEFERRED: grid generation (<c>GenerateGrid</c>, needs the grid package), music
/// (<c>ProcessMusic</c>), and the black-screen transition + event-log messages (UI system).
/// </summary>
public abstract partial class Level
{
	public abstract LevelType Type { get; }
	public string Music => GetMusic( Type );
	public virtual BBox WorldBox => new( new Vector3( -100000f ), new Vector3( 100000f ) );

	/// <summary>
	/// Music track per level. Single source of truth so the client-side music player
	/// (<see cref="MansionGame"/>) can resolve a track from the replicated <see cref="LevelType"/>
	/// without a host-side <see cref="Level"/> instance.
	/// </summary>
	public static string GetMusic( LevelType type ) => type switch
	{
		LevelType.Shop => "sounds/music/scary_quest_at_midnight.sound",
		LevelType.Mansion => "sounds/music/looming_trees_in_eerie_woods.sound",
		LevelType.Dungeon => "sounds/music/malevolent_sightings_in_the_room.sound",
		LevelType.Bathrooms => "sounds/music/depths_and_terror.sound",
		_ => null,
	};

	public LegacyUsableComponent Exit { get; set; }
	public List<NPC> Monsters { get; } = new();
	public TimeSince SinceStarted { get; set; }

	public static bool GameIsEnding { get; set; }

	protected Scene Scene => MansionGame.Instance.Scene;

	public virtual void Compute()
	{
		var players = Scene.GetAllComponents<Player>().ToList();
		if ( players.Count > 0 && players.All( x => !x.IsAlive ) && !GameIsEnding )
		{
			MansionGame.RestartGame();
			GameIsEnding = true;
			// TODO (UI): Eventlog "Looks like everyone died, better luck next time!"
		}
	}

	protected void RespawnAll() => MansionGame.Instance.RespawnAll();

	public virtual async Task Start()
	{
		await GameTask.Yield();

		RespawnAll();

		// Companion spawning for players who bought the upgrade.
		foreach ( var player in Scene.GetAllComponents<Player>().ToList() )
		{
			if ( player.HasUpgrade( "Cartoony Sidekick" ) )
			{
				var doob = NPC.Create<Doob>( player.WorldPosition, player.WorldRotation );
				doob.Owner = player;
				player.Doob = doob;

				// Who's a Good Boy?: Doob summoned to protect this player.
				player.TrackAchievement( GameStats.AchGoodBoy );
			}
		}

		Exit?.GameObject.Destroy();

		if ( Type == LevelType.Bathrooms )
		{
			var finalDoors = Scene.GetAllComponents<ValidFinalDoorPosition>().ToList();
			var spot = MansionGame.Random.FromList( finalDoors, null );
			if ( spot is not null )
				Exit = FinalDoor.Create( spot.WorldPosition, spot.WorldRotation );
		}
		else
		{
			var trapdoors = Scene.GetAllComponents<ValidTrapdoorPosition>().Where( x => x.LevelType == Type ).ToList();
			var spot = MansionGame.Random.FromList( trapdoors, null );
			if ( spot is not null )
				Exit = Trapdoor.Create( spot.WorldPosition, spot.WorldRotation );
		}

		foreach ( var spawner in Scene.GetAllComponents<LootSpawner>().Where( x => x.LevelType == Type ).ToList() )
			spawner.SpawnLoot();

		foreach ( var door in Scene.GetAllComponents<Door>().Where( x => x.LevelType == Type ).ToList() )
			door.Close();

		await GameTask.DelayRealtimeSeconds( 1f );

		await GenerateGrid();

		GameIsEnding = false;
		SinceStarted = 0f; // reset the level clock (used by the Slipped on a Soap achievement)
		MansionGame.Instance.TimerStart();
	}

	public virtual async Task End()
	{
		await GameTask.Yield();

		MansionGame.Instance?.ShowBlackScreen( 2f, 1f, 1f );

		Exit?.GameObject.Destroy();
		Exit = null;

		foreach ( var monster in Monsters.ToList() )
			RemoveMonster( monster );

		foreach ( var spawner in Scene.GetAllComponents<LootSpawner>().ToList() )
			spawner.DeleteLoot();

		foreach ( var door in Scene.GetAllComponents<Door>().Where( x => WorldBox.Contains( x.WorldPosition ) ).ToList() )
			door.Close();

		foreach ( var loot in Scene.GetAllComponents<Loot>().ToList() )
			loot.GameObject.Destroy();

		MansionGame.Instance.TimerStop();

		foreach ( var doob in Scene.GetAllComponents<Doob>().ToList() )
		{
			if ( doob.Owner.IsValid() )
				doob.Owner.Doob = null;

			doob.GameObject.Destroy();
		}
	}

	public virtual void RegisterMonster( NPC monster ) => Monsters.Add( monster );

	public virtual void RemoveMonster( NPC monster )
	{
		Monsters.Remove( monster );
		if ( monster.IsValid() )
			monster.GameObject.Destroy();
	}

	public static Type GetClrType( LevelType type ) => type switch
	{
		LevelType.Shop => typeof( ShopLevel ),
		LevelType.Mansion => typeof( MansionLevel ),
		LevelType.Dungeon => typeof( DungeonLevel ),
		LevelType.Bathrooms => typeof( BathroomsLevel ),
		_ => null,
	};
}
rue.house / Levels/ShopLevel.cs
Game game
using System.Linq;
using System.Threading.Tasks;
using Sandbox;

namespace BrickJam;

public sealed class ShopLevel : Level
{
	public override LevelType Type => LevelType.Shop;

	public override async Task Start()
	{
		// Shop is a safe hub - no base.Start() (no exit/monsters/loot/grid).
		await GameTask.Yield();

		RespawnAll();
		MansionGame.Instance.TimerStop();
	}
}
rue.house / NPC/Specter.cs
Game game
using System;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;
using GridAStar;

namespace BrickJam;

/// <summary>
/// Specter monster - sinks into the floor and rises elsewhere. Scene-System port of legacy <c>Specter</c>,
/// now on grid A*: it chases directly, and on a long idle teleports to a random grid cell (sink → reposition
/// → rise). The lamp light flicker (legacy CapsuleLightEntity) is deferred to the effects system.
/// </summary>
[Title( "Specter" )]
[Category( "NPC" )]
public sealed partial class Specter : NPC
{
	public override string ModelPath { get; set; } = "models/specter/specter.vmdl";
	public override float WalkSpeed { get; set; } = 120f;
	public override float RunSpeed { get; set; } = 320f;
	public override float MaxVisionAngle { get; set; } = 240f;
	public override float MaxVisionRange { get; set; } = 1000f;
	public override float MaxVisionRangeWhenChasing { get; set; } = 1000f;
	public override float MaxVisionAngleWhenChasing { get; set; } = 180f;
	public override float MaxRememberTime { get; set; } = 3f;
	public override string IdleSound => "sounds/specter/spectermoan.sound";
	public override float IdleVolume => 1.5f;
	public override string AttackSound => "sounds/specter/spectermoan.sound";
	public override float AttackVolume => 2f;

	public float TimeToTeleport => 2f;
	public bool IsLowering => LastTeleport <= TimeToTeleport / 2f + 0.5f;
	public bool IsRising => LastTeleport <= TimeToTeleport + 1f && LastTeleport > TimeToTeleport / 2f + 0.5f;
	public bool IsTeleporting => IsLowering || IsRising;

	[Sync] public TimeSince LastTeleport { get; set; } = 999f;

	private static readonly Color LampColor = new( 1f, 0.55f, 0.2f );
	private PointLight lamp;

	protected override void OnStart()
	{
		base.OnStart();

		// Atmospheric lamp light (legacy CapsuleLightEntity). Purely visual, so each client builds its OWN
		// non-networked light and flickers it locally in OnUpdate - NPC logic (Think) is host-only and the
		// flicker shouldn't depend on replicated state.
		var lampGo = new GameObject( true, "Lamp" ) { Parent = GameObject };
		lampGo.LocalPosition = Vector3.Up * 40f;
		lamp = lampGo.Components.Create<PointLight>();
		lamp.LightColor = LampColor;
		lamp.Radius = 350f;
		lamp.Shadows = false; // atmospheric glow - skip the (6-face) shadow pass for this point light
	}

	protected override void OnUpdate()
	{
		if ( !lamp.IsValid() )
			return;

		// Eerie flicker; fade the lamp out while the specter is sunk into the floor (teleporting).
		var t = Time.Now;
		var flicker = 0.7f + 0.18f * MathF.Sin( t * 27f ) + 0.12f * MathF.Sin( t * 11.3f );
		var visible = IsTeleporting ? 0f : 1f;

		lamp.LightColor = LampColor * (flicker * visible * 4f);
	}

	public override void ComputeIdleAndSeek()
	{
		// While sinking/rising we don't make new decisions; ComputeMotion drives the vertical move.
		if ( IsTeleporting )
			return;

		if ( InVision.Count > 0 )
		{
			Target = InVision.OrderBy( x => x.Key.WorldPosition.Distance( WorldPosition ) ).FirstOrDefault().Key;

			if ( Target is Player player && player.Doob.IsValid() )
				Target = player.Doob;

			LastTarget = Target;
		}
		else
		{
			Target = null;

			if ( !IsFollowingPath && nextIdle && CurrentGrid is not null )
			{
				var isLongIdle = MansionGame.Random.NextSingle() <= 0.2f;
				var allCells = CurrentGrid.AllCells.ToList();

				Cell chosen = null;
				for ( var tried = 0; tried < 20 && allCells.Count > 0; tried++ )
				{
					var candidate = MansionGame.Random.FromList( allCells, null );
					if ( candidate is null )
						break;

					var dist = candidate.Position.Distance( WorldPosition );
					if ( isLongIdle ? dist >= 1000f : (dist >= 400f && dist <= 1000f) )
					{
						chosen = candidate;
						break;
					}
				}

				if ( chosen != null )
				{
					if ( isLongIdle )
						Teleport( chosen.Position );
					else
						NavigateTo( chosen );
				}

				nextIdle = MansionGame.Random.NextSingle() * 1f + 1f;
				LastTarget = null;
			}
		}

		if ( Target.IsValid() && Target.WorldPosition.Distance( WorldPosition ) <= KillRange )
		{
			if ( Target is Player p )
				_ = CatchPlayer( p );
			else if ( Target is Doob d )
				_ = CatchDoob( d );
		}

		if ( nextIdleSound && !string.IsNullOrEmpty( IdleSound ) )
		{
			SoundExtensions.BroadcastPlay( IdleSound, WorldPosition, IdleVolume );
			nextIdleSound = MansionGame.Random.NextSingle() * 4f + 4f;
		}
	}

	public override void ComputeMotion()
	{
		if ( !IsTeleporting )
		{
			base.ComputeMotion();
			return;
		}

		// Sink into / rise out of the floor under manual control (off the grid).
		if ( IsLowering )
			WorldPosition += Vector3.Down * CollisionHeight * Time.Delta / (TimeToTeleport / 2f);
		else if ( IsRising )
			WorldPosition += Vector3.Up * CollisionHeight * Time.Delta / (TimeToTeleport / 2f);
	}

	public async void Teleport( Vector3 position )
	{
		LastTeleport = 0;
		MansionGame.Instance?.PlayEffect( "prefabs/particles/specter_teleport.prefab", WorldPosition, Rotation.Identity );

		await Task.DelayRealtimeSeconds( TimeToTeleport * 0.5f + 0.5f );

		WorldPosition = position + Vector3.Down * CollisionHeight;
	}
}
rue.house / UI/GroundLootPanel.razor
Game game
@using System
@using Sandbox
@using Sandbox.UI
@namespace BrickJam.UI
@inherits PanelComponent

<root>
	@if ( Loot.IsValid() )
	{
		<div class="container">
			<span>@Loot.FullName</span>
			<div class="row">
				<span class="currency">$</span><span>@($"{Loot.MonetaryValue:n0}")</span>
			</div>
		</div>
	}
</root>

@code {
	private Loot loot;
	private Loot Loot => loot ??= GetComponentInParent<Loot>();

	protected override int BuildHash() => HashCode.Combine( Loot?.FullName, Loot?.MonetaryValue ?? 0 );
}

<style>
	GroundLootPanel {
		transition: transform 0.5s ease-in-out;
		justify-content: center;
		align-items: center;
		width: 100%;
		height: 100%;

		.container {
			padding: 10px;
			padding-right: 30px;
			padding-left: 30px;
			font-size: 32px;
			align-items: center;
			font-family: "alagard";
			flex-direction: column;
			color: white;
			text-shadow: 3px 3px 0px black;
			background: linear-gradient(to left, rgba(black, 0) 0%, rgba(black, 0.5) 20%, rgba(black, 0.5) 80%, rgba(black, 0) 100%);

			.currency {
				padding-right: 4px;
				font-size: 22px;
				top: 5px;
				color: rgba(50, 205, 50, 1);
			}
		}

		&:outro {
			transform: scale(0);
		}

		&:intro {
			transform: scale(0);
		}
	}
</style>
rue.house / UI/Hud.razor
Game game
@using System
@using Sandbox
@using Sandbox.UI
@namespace BrickJam.UI
@inherits PanelComponent

<root>
	@{
		var player = Player.Local;

		var isSpectator = player?.Spectating ?? false;
		var isPlayer = player?.IsValid() ?? false;
	}

	<MoneyCounter />
	<Eventlog />
	<PlayerCounter />
	<StunIndicator />
	<QuickPing />
	<SubtitlesList />
	<Lockpicker />
	<BlackScreen />
	
	<div class="input-hints">
		@if ( isSpectator )
		{
			<div class="hint"><inputglyph action="StopFollowing"/>Stop following</div>
			<div class="hint"><inputglyph action="FollowNext"/>Next player</div>
			<div class="hint"><inputglyph action="FollowPrevious"/>Previous player</div>
		}
		else
		{
			<div class="hint"><inputglyph action="inventory"/>Inventory</div>
			<div class="hint"><inputglyph action="chat"/>Chat</div>
			<div class="hint"><inputglyph action="ping"/>Quick Ping</div>
		}
	</div>

	@if ( player.IsValid() )
	{
		<Inventory />
		<Shop />
		<TextInput Ghost="Say something..." class="chat-input" @ref="ChatInput" onsubmit=@OnChatSubmit />

		@if ( !player.SeenTips )
		{
			<TutorialTips />
		}
	}

	@if ( player.IsValid() && player.IsAlive && !LockpickerBus.IsOpen )
	{
		<div class="crosshair">
			<div class="mark">+</div>
			<RadialProgress />
			<InteractionTip />
		</div>
	}

	@if ( player.IsValid() && !player.IsAlive )
	{
		<div class="death">
			<span class="title">YOU'RE DEAD!</span>
		</div>
	}
</root>

@code {
	private TextInput ChatInput { get; set; }

	protected override void OnUpdate()
	{
		if ( ChatInput is null )
			return;

		if ( Input.Pressed( "chat" ) )
		{
			ChatInput.Focus();
			ChatInput.AddClass( "visible" );
		}
	}

	private void OnChatSubmit()
	{
		if ( ChatInput is null )
			return;

		var text = ChatInput.Text;
		if ( !string.IsNullOrWhiteSpace( text ) )
			Player.Local?.Say( text );

		ChatInput.Text = "";
		ChatInput.RemoveClass( "visible" );
	}

	protected override int BuildHash() => HashCode.Combine(
		Player.Local,
		Player.Local?.IsAlive ?? false,
		LockpickerBus.IsOpen );
}

<style>
	Hud {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		display: flex;
		flex-direction: column;
		align-items: center;
		font-family: "alagard";
		justify-content: flex-start;

		.crosshair {
			position: absolute;
			top: 0; left: 0;
			width: 100%;
			height: 100%;
			flex-direction: column;
			align-items: center;
			justify-content: center;

			.mark {
				color: white;
				font-size: 28px;
				text-shadow: 2px 2px 0px black;
			}
		}

		.input-hints {
			position: absolute;
			bottom: 30px;
			right: 0px;
			align-items: flex-end;
			flex-direction: column;
			z-index: 3;

			.hint {
				color: white;
				height: 32px;
				justify-content: center;
				font-size: 32px;
				padding-left: 50px;
				padding-top: 10px;
				padding-bottom: 40px;
				padding-right: 10px;
				background: linear-gradient(to left, rgba(black, 0.5) 0%, rgba(black, 0.2) 75%, rgba(black, 0) 100%);
				margin-top: 5px;
				text-shadow: 3px 3px 0px black;

				InputGlyph {
					width: 32px;
					aspect-ratio: 1;
					margin-right: 10px;
				}
			}
		}

		.chat-input {
			position: absolute;
			width: 450px;
			min-height: 20px;
			font-size: 24px;
			opacity: 0;
			pointer-events: all;
			background-color: rgba(37, 64, 98, 1);
			box-shadow: 3px 3px 0px 0px rgba(17, 44, 78, 1);
			transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
			transform: translate(-50% 200px);
			text-shadow: 3px 3px 0px black;
			left: 50%;
			top: 50%;
			margin-top: 40px;

			&.visible {
				opacity: 1;
				transform: translate(-50% 0px);
			}
		}

		.death {
			position: absolute;
			width: 100%;
			height: 100%;
			left: 0px;
			top: 0px;
			color: white;
			font-size: 32px;
			z-index: 2;
			backdrop-filter: grayscale(100%);
			justify-content: center;
			transition: opacity 1s ease-in-out;
			opacity: 0;
			text-shadow: 4px 4px 0px black;

			&.visible {
				opacity: 1;
			}

			.container {
				flex-direction: column;
				align-items: center;
			}

			span {
				background: linear-gradient(to left, rgba(black, 0) 0%, rgba(black, 0.4) 20%, rgba(black, 0.4) 80%, rgba(black, 0) 100%);
				padding: 10px;
				padding-left: 40px;
				padding-right: 40px;
			}

			.title {
				color: red;
				font-size: 64px;
				margin-top: 150px;
				margin-bottom: 20px;
			}
		}
	}
</style>
rue.house / UI/Nametag.razor
Game game
@using System
@using Sandbox
@using Sandbox.UI
@using WorldPanel = Sandbox.WorldPanel
@namespace BrickJam.UI
@inherits PanelComponent

<root>
	@if ( !IsLocal && Player.IsValid() && Player.IsAlive )
	{
		<div class="container">
			<span>@Name</span>
		</div>
	}
</root>

@code {
	private Player player;
	private Player Player => player ??= GetComponentInParent<Player>();

	// Don't draw your own nametag in first person.
	private bool IsLocal => Player == Player.Local;

	private string Name => Player.IsValid() && Player.Network.Owner is { } owner ? owner.DisplayName : "player";

	protected override int BuildHash() => HashCode.Combine( IsLocal, Name, Player?.IsAlive ?? false );
}

<style>
	Nametag {
		transition: transform 0.5s ease-in-out;
		justify-content: center;
		align-items: center;
		width: 100%;
		height: 100%;

		.container {
			padding: 10px;
			padding-right: 30px;
			padding-left: 30px;
			font-size: 32px;
			align-items: center;
			font-family: "alagard";
			flex-direction: column;
			color: white;
			text-shadow: 3px 3px 0px black;
			background: linear-gradient(to left, rgba(black, 0) 0%, rgba(black, 0.5) 20%, rgba(black, 0.5) 80%, rgba(black, 0) 100%);
		}

		&:outro {
			transform: scale(0);
		}

		&:intro {
			transform: scale(0);
		}
	}
</style>
rue.house / UI/PingBus.cs
Game game
using System;
using Sandbox;

namespace BrickJam.UI;

/// <summary>Decouples gameplay from the Razor <c>QuickPing</c> panel (see <see cref="EventlogBus"/>).</summary>
public static class PingBus
{
	public enum PingType { Enemy, Exit, Loot, Other }

	public static event Action<Vector3, PingType> OnPing;

	public static void Post( Vector3 worldPosition, PingType type ) => OnPing?.Invoke( worldPosition, type );
}
rue.house / UI/SubtitlesList.razor
Game game
@using System
@using System.Linq
@using System.Collections.Generic
@using Sandbox
@using Sandbox.UI
@namespace BrickJam.UI
@inherits Panel

<root>
	@foreach ( var line in lines )
	{
		<div class="subtitle">
			@if ( !string.IsNullOrEmpty( line.Speaker ) )
			{
				<span class="speaker">@line.Speaker:</span>
			}
			<span class="text">@line.Text</span>
		</div>
	}
</root>

@code {
	private readonly List<(string Speaker, string Text, TimeUntil Expire)> lines = new();

	public SubtitlesList()
	{
		SubtitleBus.OnSubtitle += Add;
	}

	public override void OnDeleted()
	{
		base.OnDeleted();
		SubtitleBus.OnSubtitle -= Add;
	}

	private void Add( string speaker, string text, float duration )
	{
		lines.Add( (speaker, text, duration) );
		if ( lines.Count > 3 )
			lines.RemoveAt( 0 );
	}

	public override void Tick()
	{
		lines.RemoveAll( l => l.Expire );
	}

	protected override int BuildHash() => HashCode.Combine( lines.Count, lines.LastOrDefault().Text );
}

<style>
	SubtitlesList {
		position: absolute;
		left: 0; right: 0;
		bottom: 60px;
		flex-direction: column;
		align-items: center;

		.subtitle {
			flex-direction: row;
			padding: 6px 16px;
			margin-top: 4px;
			background-color: rgba(0,0,0,0.6);
			font-size: 26px;
			text-shadow: 2px 2px 0px black;

			.speaker { color: rgba(255,220,80,1); margin-right: 8px; }
			.text { color: white; }
		}
	}
</style>
rue.house / ui/controls/switchcontrol.razor.scss
Game game
.switchcontrol
{
    flex-direction: row;
    width: 100px;
    min-height: 24px;
    align-items: center;
    cursor: pointer;

    .switch-frame
    {
        flex-grow: 0;
        flex-shrink: 1;
        width: 48px;
        height: 16px;
        background-color: #fff1;
        margin: 0px 5px;
        align-items: center;
        border-radius: 100px;
        transition: all 0.4s linear;

        .switch-inner
        {
            position: relative;
            flex-grow: 0;
            flex-shrink: 1;
            background-color: #999;
            width: 25px;
            height: 25px;
            border-radius: 100px;
            left: 20%;
            transform: translateX( -50% );
            transition: all 0.3s ease-out;
        }
    }

    &.active
    {
        .switch-frame
        {
            background-color: #fffa;
        }

        .switch-inner
        {
            left: 80%;
            background-color: #fff;
        }
    }
}
rue.house / styles/form.scss
Game game
$form-control-height: 28px !default;

@import "form/_checkbox.scss";
@import "form/_switch.scss";
@import "form/_dropdown.scss";
@import "form/_coloreditor.scss";
@import "form/_colorproperty.scss";

.form
{
	flex-direction: column;
	align-items: stretch;
	justify-content: flex-start;
	overflow: scroll;
}

.field-group
{
	flex-direction: column;
	flex-shrink: 0;
}

.field-header
{
	flex-shrink: 0;
}

.field
{
	color: white;
	font-size: 14px;
	flex-shrink: 0;
	flex-grow: 0;

	> .label
	{
		flex-grow: 0;
		flex-shrink: 0;
		font-weight: 600;
		opacity: 0.4;
		width: 20%;
		font-size: 13px;
	}

	> .control
	{
		flex-shrink: 0;
		flex-grow: 1;
		flex-direction: column;
	}
}

.is-vertical > .field, .field.is-vertical
{
	flex-direction: column;

	> .label
	{
		width: auto;
		height: auto;
	}
}
rue.house / ui/controls/color/colorpickercontrol.cs.scss
Game game
ColorPickerControl
{
	flex-direction: column;
	flex-shrink: 0;
	gap: 0.5rem;
	margin: 1rem;
}
rue.house / ui/controls/enumcontrol.cs.scss
Game game
EnumControl
{
	gap: 2px;
	flex-grow: 1;
}

EnumControl DropDown,
EnumControl ButtonGroup
{
	border-radius: 8px;
	background-color: #000a;
	flex-grow: 1;
}

EnumControl DropDown
{
	flex-grow: 1;
	min-height: 32px;
}

EnumControl ButtonGroup
{
	border-radius: 12px;
	overflow: hidden;
	min-height: 32px;

	Button
	{
		flex-grow: 1;
		justify-content: center;
		align-items: center;
		gap: 4px;
		color: #aaa;
		font-size: 1rem;
		cursor: pointer;

		.icon
		{
			color: #08f;
		}

		&:hover
		{
			color: #ddd;

			.icon
			{
				color: #3af;
			}
		}

		&:active
		{
			background-color: #04a;
			color: white;
			transform: translateX( 1px ) translateY( 1px );

			.icon
			{
				color: #fff;
			}
		}

		&.active
		{
			background-color: #08f;
			color: white;
			pointer-events: none;

			.icon
			{
				color: #fff;
			}
		}
	}
}
rue.house / ui/controls/color/coloralphacontrol.cs.scss
Game game
ColorAlphaControl
{
	gap: 0.5rem;
	flex-grow: 1;
	pointer-events: all;
	background: linear-gradient( to right, black, white );
	border-radius: 4px;
	padding: 2px;
	height: 12px;
	position: relative;
	cursor: pointer;
	border: 1px solid #333;

	&:hover
	{
		border: 1px solid #08f;
	}

	&:active
	{
		border: 1px solid #fff;
	}

	.handle
	{
		top: -5px;
		bottom: -5px;
		aspect-ratio: 1;
		border-radius: 100px;
		border: 2px solid #444;
		position: absolute;
		background-color: white;
		box-shadow: 2px 2px 16px #000a;
		transform: translateX( -50% );
		pointer-events: none;
	}
}
rue.house / ui/controls/color/colorsaturationvaluecontrol.cs.scss
Game game
ColorSaturationValueControl
{
	width: 240px;
	height: 240px;
	background-color: red;
	position: relative;
	border-radius: 4px;
	cursor: pointer;
	border: 1px solid #333;

	&:hover
	{
		border: 1px solid #08f;
	}

	&:active
	{
		border: 1px solid #fff;
	}

	.handle
	{
		width: 16px;
		height: 16px;
		border-radius: 100px;
		border: 2px solid #444;
		position: absolute;
		background-color: white;
		box-shadow: 2px 2px 16px #000a;
		transform: translateX( -50% ) translateY( -50% );
		pointer-events: none;
		z-index: 100;
		z-index: 100;
	}

	.gradient
	{
		position: absolute;
		width: 100%;
		height: 100%;
		border-radius: 4px;
		background: linear-gradient( to right, white, rgba( 255, 255, 255, 0 ) );

		&:after
		{
			content: "";
			position: absolute;
			width: 100%;
			height: 100%;
			border-radius: 4px;
			background: linear-gradient( to top, black, rgba( 0, 0, 0, 0 ) );
		}
	}
}
rue.house / Grid/Grid.cs
Game game
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;

namespace GridAStar;

public partial class Grid : IValid
{
	public static Grid Main
	{
		get => Grids.GetValueOrDefault( "main" );
		set
		{
			if ( Grids.ContainsKey( "main" ) )
				Grids["main"] = value;
			else
				Grids.Add( "main", value );
		}
	}

	public static Dictionary<string, Grid> Grids { get; set; } = new();

	/// <summary>The scene this grid traces against. Set on creation (Scene-System port).</summary>
	public Scene Scene { get; set; }

	// --- Generation diagnostics ---
	public static int DebugCastsHit;
	public static int DebugAngleRejected;
	public static int DebugOutOfBounds;

	public GridBuilder Settings { get; internal set; }
	public string Identifier => Settings.Identifier;
	public Dictionary<IntVector2, List<Cell>> CellStacks { get; internal set; } = new();
	public IEnumerable<Cell> AllCells => CellStacks.Values.SelectMany( list => list );
	public Vector3 Position => Settings.Position;
	public BBox Bounds => Settings.Bounds;
	public BBox RotatedBounds => Bounds.GetRotatedBounds( Rotation );
	public BBox WorldBounds => RotatedBounds.Translate( Position );
	public Transform Transform => new Transform( WorldBounds.Center, AxisRotation );
	public Rotation Rotation => Settings.Rotation;
	public bool AxisAligned => Settings.AxisAligned;
	public float StandableAngle => Settings.StandableAngle;
	public float StepSize => Settings.StepSize;
	public float CellSize => Settings.CellSize;
	public float HeightClearance => Settings.HeightClearance;
	public float WidthClearance => Settings.WidthClearance;
	public bool GridPerfect => Settings.GridPerfect;
	public bool StaticOnly => Settings.StaticOnly;
	public float MaxDropHeight => Settings.MaxDropHeight;
	public List<JumpDefinition> JumpDefinitions => Settings.JumpDefinitions;
	public int MinNeighbourCount => Settings.MinNeighbourCount;
	public bool IgnoreConnectionsForJumps => Settings.IgnoreConnectionsForJumps;
	public bool IgnoreLOSForJumps => Settings.IgnoreLOSForJumps;
	public bool CylinderShaped => Settings.CylinderShaped;
	public float Tolerance => GridPerfect ? 0.001f : 0f;
	public Rotation AxisRotation => AxisAligned ? new Rotation() : Rotation;
	public int MinimumColumn => WorldBounds.Mins.ToIntVector2( CellSize ).y;
	public int MaximumColumn => WorldBounds.Maxs.ToIntVector2( CellSize ).y;
	public int Columns => MaximumColumn - MinimumColumn;
	public int MinimumRow => WorldBounds.Mins.ToIntVector2( CellSize ).x;
	public int MaximumRow => WorldBounds.Maxs.ToIntVector2( CellSize ).x;
	public int Rows => MaximumRow - MinimumRow;
	bool IValid.IsValid { get; }

	public Grid()
	{
		Settings = new GridBuilder();
	}

	public Grid( GridBuilder settings )
	{
		Settings = settings;
	}

	public void Print( string message ) => Print( Identifier, message );
	public static void Print( string identifier, string message ) => Log.Info( $"Grid '{identifier}': {message}" );

	public BBox ToWorld( BBox bounds ) => bounds.GetRotatedBounds( AxisRotation ).Translate( WorldBounds.Center );
	public BBox ToLocal( BBox bounds ) => bounds.GetRotatedBounds( AxisRotation.Inverse ).Translate( -WorldBounds.Center );

	public IntVector2 PositionToCoordinates( Vector3 position ) => (position - WorldBounds.Mins - CellSize / 2).ToIntVector2( CellSize );

	/// <summary>Find the nearest cell from a position even if outside the grid (expensive).</summary>
	public Cell GetNearestCell( Vector3 position, bool onlyBelow = true, bool unoccupiedOnly = false )
	{
		var validCells = AllCells;

		if ( unoccupiedOnly )
			validCells = validCells.Where( x => !x.Occupied );
		if ( onlyBelow )
			validCells = validCells.Where( x => x.Vertices.Min() - Math.Max( HeightClearance, StepSize ) <= position.z );

		return validCells.OrderBy( x => x.Position.DistanceSquared( position ) )
			.FirstOrDefault();
	}

	public Cell GetCellInArea( Vector3 position, float width, bool onlyBelow = true, bool withinStepRange = true )
	{
		var cellsToCheck = (int)Math.Ceiling( width / CellSize ) * 2;
		for ( int y = 0; y <= cellsToCheck; y++ )
		{
			var spiralY = MathAStar.SpiralPattern( y );
			for ( int x = 0; x <= cellsToCheck; x++ )
			{
				var spiralX = MathAStar.SpiralPattern( x );
				var cellFound = GetCell( position + AxisRotation.Forward * spiralX * CellSize + AxisRotation.Right * spiralY * CellSize + Vector3.Up * StepSize, onlyBelow );

				if ( cellFound == null ) continue;

				if ( withinStepRange )
					if ( position.z - cellFound.Position.z <= Math.Max( HeightClearance, StepSize ) ) return cellFound; else continue;

				return cellFound;
			}
		}

		return null;
	}

	public Cell GetCell( Vector3 position, bool onlyBelow = true ) => GetCell( PositionToCoordinates( position ), onlyBelow ? position.z : WorldBounds.Maxs.z );

	public Cell GetCell( IntVector2 coordinates, float height )
	{
		var cellsAtCoordinates = CellStacks.GetValueOrDefault( coordinates );

		if ( cellsAtCoordinates == null ) return null;

		// Return the cell CLOSEST to the query height among the candidates, not the first match. A column on
		// a spiral staircase / multi-floor area stacks several cells at the same XY; the original first-match
		// returned an arbitrary one (often the bottom of the spiral), so an NPC partway up got a path from the
		// wrong height and couldn't follow it. Candidate window (<= height + clearance) is unchanged.
		Cell best = null;
		var bestDist = float.MaxValue;

		foreach ( var cell in cellsAtCoordinates )
		{
			if ( cell.Vertices.Min() - Math.Max( HeightClearance, StepSize ) >= height )
				continue;

			var dist = Math.Abs( cell.Position.z - height );
			if ( dist < bestDist )
			{
				bestDist = dist;
				best = cell;
			}
		}

		return best;
	}

	public void AddCell( Cell cell )
	{
		if ( cell == null ) return;
		var coordinates = cell.GridPosition;
		if ( !CellStacks.ContainsKey( coordinates ) )
			CellStacks.Add( coordinates, new List<Cell>() { cell } );
		else
			if ( !CellStacks[coordinates].Any( x => Math.Abs( x.Position.z - cell.Position.z ) < Math.Max( HeightClearance, StepSize ) ) )
			CellStacks[coordinates].Add( cell );
	}

	public Cell GetCellInDirection( Cell startingCell, Vector3 direction, int numOfCellsInDirection = 1 ) => GetCell( startingCell.Position + direction * CellSize * numOfCellsInDirection );

	public Cell GetNeighbourInDirection( Cell cell, Vector3 direction )
	{
		var horizontalDirection = direction.WithZ( 0 ).Normal;
		var localCoordinates = horizontalDirection.ToIntVector2();
		var coordinatesToCheck = cell.GridPosition + localCoordinates;

		var cellsAtCoordinates = CellStacks.GetValueOrDefault( coordinatesToCheck );

		if ( cellsAtCoordinates == null ) return null;

		foreach ( var cellAtCoordinate in cellsAtCoordinates )
			if ( cell.IsNeighbour( cellAtCoordinate ) && cell != cellAtCoordinate )
				return cellAtCoordinate;

		return null;
	}

	/// <summary>Returns if there's a valid, unoccupied, and direct line of sight from a cell to another</summary>
	public bool LineOfSight( Cell startingCell, Cell endingCell, Component pathCreator = null, bool debugShow = false )
	{
		var startingPosition = startingCell.Position;
		var endingPosition = endingCell.Position;
		var distanceInSteps = (int)Math.Ceiling( startingPosition.Distance( endingPosition ) / CellSize );

		if ( pathCreator == null && startingCell.Occupied ) return false;
		if ( pathCreator != null && startingCell.Occupied && startingCell.OccupyingEntity != pathCreator ) return false;

		if ( pathCreator == null && endingCell.Occupied ) return false;
		if ( pathCreator != null && endingCell.Occupied && endingCell.OccupyingEntity != pathCreator ) return false;

		Cell lastCell = startingCell;
		for ( int i = 0; i <= distanceInSteps; i++ )
		{
			var direction = (endingPosition - lastCell.Position).Normal;
			var cellToCheck = GetNeighbourInDirection( lastCell, direction );

			if ( cellToCheck == null ) return false;
			if ( cellToCheck == endingCell ) return true;
			if ( cellToCheck == lastCell ) continue;
			if ( pathCreator == null && cellToCheck.Occupied ) return false;
			if ( pathCreator != null && cellToCheck.Occupied && cellToCheck.OccupyingEntity != pathCreator ) return false;
			if ( !cellToCheck.IsNeighbour( lastCell ) ) return false;

			lastCell = cellToCheck;

			if ( debugShow )
				lastCell.Draw( 2f, false, false, false );
		}

		return true;
	}

	/// <summary>Can you roughly walk towards the cell without it being a direct line of sight</summary>
	public bool IsDirectlyWalkable( Cell startingCell, Cell endingCell, float maxDistanceFromDirectPath = 150f, Component pathCreator = null, bool withConnections = true )
	{
		if ( startingCell == null || endingCell == null ) return false;

		var currentCell = startingCell;
		var directPath = new Line( startingCell.Position.WithZ( 0 ), endingCell.Position.WithZ( 0 ) );
		List<Cell> cellsChecked = new();

		if ( pathCreator == null && startingCell.Occupied ) return false;
		if ( pathCreator != null && startingCell.Occupied && startingCell.OccupyingEntity != pathCreator ) return false;

		if ( pathCreator == null && endingCell.Occupied ) return false;
		if ( pathCreator != null && endingCell.Occupied && endingCell.OccupyingEntity != pathCreator ) return false;

		while ( currentCell != endingCell && directPath.Distance( currentCell.Position.WithZ( 0 ) ) <= maxDistanceFromDirectPath )
		{
			var cellToCheck = withConnections ? currentCell.GetClosestNeighbourAndConnection( endingCell.Position ) : currentCell.GetClosestNeighbour( endingCell.Position );

			if ( cellToCheck == null ) return false;
			if ( cellsChecked.Contains( cellToCheck ) ) return false;
			if ( pathCreator == null && cellToCheck.Occupied ) return false;
			if ( pathCreator != null && cellToCheck.Occupied && cellToCheck.OccupyingEntity != pathCreator ) return false;

			if ( cellToCheck == endingCell ) return true;

			cellsChecked.Add( currentCell );
			currentCell = cellToCheck;
		}

		return false;
	}

	public bool IsInsideBounds( Vector3 point ) => Bounds.IsRotatedPointWithinBounds( Position, point, Rotation );
	public bool IsInsideCylinder( Vector3 point ) => Bounds.IsInsideSquishedRotatedCylinder( Position, point, Rotation );

	public void Initialize()
	{
		if ( Grids.ContainsKey( Identifier ) )
		{
			if ( Grids[Identifier] != null )
				Grids[Identifier].Delete( true );

			Grids[Identifier] = this;
		}
		else
			Grids.Add( Identifier, this );
	}

	public void Delete( bool deleteSave = false )
	{
		if ( Grids.ContainsKey( Identifier ) )
		{
			Grids[Identifier] = null;
			Grids.Remove( Identifier );
		}
	}

	public List<Cell> GetCellsInBBox( BBox bbox )
	{
		var cells = new List<Cell>();

		foreach ( var cell in AllCells )
			if ( bbox.Contains( cell.Position ) )
				cells.Add( cell );

		return cells;
	}

	public override int GetHashCode() => Settings.GetHashCode();

	/// <summary>Gives the edge tag to all cells with less than 8 neighbours</summary>
	public async Task AssignEdgeCells( int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = "", string tagToAssign = "edge" ) => await assignEdgeCellsInternal( AllCells.ToList(), maxNeighourCount, threadsToUse, clearTags, tagToExclude, tagToAssign );

	public async Task AssignEdgeCells( BBox bounds, int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = "", string tagToAssign = "edge" ) => await assignEdgeCellsInternal( GetCellsInBBox( bounds ), maxNeighourCount, threadsToUse, clearTags, tagToExclude, tagToAssign );

	internal async Task assignEdgeCellsInternal( List<Cell> cells, int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = "", string tagToAssign = "edge" )
	{
		var cellsCount = cells.Count();
		threadsToUse = Math.Max( 1, threadsToUse );
		var cellsEachThread = (int)(cellsCount / threadsToUse);
		var lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));
		List<Task> tasks = new();

		for ( int i = 0; i < threadsToUse; i++ )
		{
			var curentThread = i;

			tasks.Add( GameTask.RunInThreadAsync( () =>
			{
				var cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;
				var cellsToCheck = cells.Skip( cellsEachThread * curentThread ).Take( cellsRange );

				foreach ( var cell in cellsToCheck )
				{
					if ( clearTags )
						cell.Tags.Remove( tagToAssign );

					var neighbours = cell.GetNeighbours();

					if ( tagToExclude != "" )
						neighbours = neighbours.Where( x => !x.Tags.Has( tagToExclude ) );

					if ( neighbours.Count() < maxNeighourCount )
						cell.Tags.Add( tagToAssign );
				}
			} ) );
		}

		await GameTask.WhenAll( tasks );
	}

	/// <summary>Adds the droppable connection to cells you can drop from</summary>
	public async Task AssignDroppableCells( int threadsToUse = 1 ) => await internalAssignDroppableCells( CellsWithTag( "edge" ).ToList(), threadsToUse );

	public async Task AssignDroppableCells( BBox bounds, int threadsToUse = 1 ) => await internalAssignDroppableCells( CellsWithTag( bounds, "edge" ).ToList(), threadsToUse );

	internal async Task internalAssignDroppableCells( List<Cell> cells, int threadsToUse = 1 )
	{
		var allCells = cells;
		var cellsCount = allCells.Count();
		threadsToUse = Math.Max( 1, threadsToUse );
		var cellsEachThread = (int)(cellsCount / threadsToUse);
		var lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));
		List<Task> tasks = new();

		for ( int i = 0; i < threadsToUse; i++ )
		{
			var curentThread = i;

			tasks.Add( GameTask.RunInThreadAsync( () =>
			{
				var cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;
				var cellsToCheck = allCells.Skip( cellsEachThread * curentThread ).Take( cellsRange );

				foreach ( var cell in cellsToCheck )
				{
					var droppableCell = cell.GetFirstValidDroppable( maxHeightDistance: MaxDropHeight );
					if ( droppableCell != null )
						cell.AddConnection( droppableCell, "drop" );
				}
			} ) );
		}

		await GameTask.WhenAll( tasks );
	}

	public IEnumerable<Cell> JumpableCandidates()
	{
		var droppedCells = CellsWithConnection( "drop" ).SelectMany( cell => cell.GetConnections( "drop" ).Select( connection => connection.Current ) );
		return CellsWithTag( "edge" ).Concat( droppedCells );
	}

	public async Task AssignJumpableCells( JumpDefinition definition, int threadsToUse = 16 ) => await internalAssignJumpableCells( JumpableCandidates().ToList(), definition, threadsToUse );

	internal async Task internalAssignJumpableCells( List<Cell> cells, JumpDefinition definition, int threadsToUse = 16 )
	{
		var allCells = cells;
		var cellsCount = allCells.Count();
		threadsToUse = Math.Max( 1, threadsToUse );
		var cellsEachThread = (int)(cellsCount / threadsToUse);
		var lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));
		List<Task> tasks = new();

		for ( int i = 0; i < threadsToUse; i++ )
		{
			var curentThread = i;

			tasks.Add( GameTask.RunInThreadAsync( () =>
			{
				var totalFraction = 1f;
				var cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;
				var cellsToCheck = allCells.Skip( cellsEachThread * curentThread ).Take( cellsRange );

				foreach ( var cell in cellsToCheck )
				{
					if ( totalFraction >= 1f )
					{
						List<Cell> connectedCells = new();
						List<AStarNode> jumpConnections = new();

						foreach ( var jumpableCell in cell.GetValidJumpables( definition, MaxDropHeight, IgnoreConnectionsForJumps, IgnoreLOSForJumps ) )
							if ( jumpableCell != null )
							{
								jumpConnections.Add( cell.AddConnection( jumpableCell, definition.Name ) );
								connectedCells.Add( jumpableCell );
							}

						foreach ( var jumpableConnection in connectedCells )
						{
							var direction = (cell.Position - jumpableConnection.Position).WithZ( 0 ).Normal;
							var jumpbackCell = jumpableConnection.GetValidJumpable( definition, direction, MaxDropHeight, IgnoreConnectionsForJumps, IgnoreLOSForJumps );

							if ( jumpbackCell != null )
								if ( !IsDirectlyWalkable( jumpbackCell, cell ) )
								{
									var duplicate = false;
									foreach ( var connection in jumpConnections )
										if ( connection.Parent.Current == jumpbackCell && connection.MovementTag == definition.Name )
											duplicate = true;
									if ( !duplicate )
										jumpConnections.Add( jumpableConnection.AddConnection( jumpbackCell, definition.Name ) );
								}
						}

						foreach ( var connection in jumpConnections )
							if ( LineOfSight( connection.Parent.Current, connection.Current ) )
								connection.Parent.Current.RemoveConnection( connection );

						totalFraction = 0f;
					}

					totalFraction += definition.GenerateFraction;
				}
			} ) );
		}

		await GameTask.WhenAll( tasks );
	}

	public Vector3 TraceParabola( Vector3 startingPosition, Vector3 horizontalVelocity, float verticalSpeed, float gravity, float maxDropHeight, int subSteps = 2 )
	{
		var horizontalDirection = horizontalVelocity.WithZ( 0 ).Normal;
		var horizontalSpeed = horizontalVelocity.WithZ( 0 ).Length;
		var maxHeight = startingPosition.z + MathAStar.ParabolaMaxHeight( verticalSpeed, gravity );
		var minHeight = maxHeight - maxDropHeight;
		var currentDistance = 1;
		var lastPositionChecked = startingPosition;

		while ( lastPositionChecked.z >= minHeight )
		{
			var horizontalOffset = CellSize * currentDistance / subSteps;
			var verticalOffset = MathAStar.ParabolaHeight( horizontalOffset, horizontalSpeed, verticalSpeed, gravity );
			var nextPositionToCheck = startingPosition + horizontalDirection * horizontalOffset + Vector3.Up * verticalOffset;

			var clearanceBBox = new BBox( new Vector3( -WidthClearance / 2f, -WidthClearance / 2f, StepSize ), new Vector3( WidthClearance / 2f, WidthClearance / 2f, HeightClearance ) );
			var jumpTrace = Scene.Trace.Box( clearanceBBox, lastPositionChecked, nextPositionToCheck )
				.WithGridSettings( Settings )
				.Run();

			if ( jumpTrace.Hit )
				return jumpTrace.EndPosition;

			lastPositionChecked = nextPositionToCheck;
			currentDistance++;
		}

		return lastPositionChecked;
	}

	public void RemoveCells( BBox bounds, bool printInfo = false )
	{
		var cellsToRemove = GetCellsInBBox( bounds );
		var count = cellsToRemove.Count();

		foreach ( var cell in cellsToRemove )
			cell.Delete();

		if ( printInfo )
			Print( $"Removed {count} cells" );
	}

	public async Task GenerateCells( BBox bounds, int threadedChunkSides = 1, bool printInfo = true )
	{
		List<Task<List<Cell>>> tasks = new();
		var totalMins = bounds.Mins;
		var totalMaxs = bounds.Maxs;
		var totalSize = bounds.Size;

		threadedChunkSides = Math.Max( 1, threadedChunkSides );

		for ( int x = 1; x <= threadedChunkSides; x++ )
		{
			for ( int y = 1; y <= threadedChunkSides; y++ )
			{
				var xOffset = totalSize.x / threadedChunkSides * x - totalSize.x / threadedChunkSides / 2;
				var yOffset = totalSize.y / threadedChunkSides * y - totalSize.y / threadedChunkSides / 2;
				var offset = new Vector3( xOffset, yOffset );
				var chunkSize = totalSize / threadedChunkSides;
				var chunkMins = totalMins + offset - chunkSize / 2;
				var chunkMaxs = totalMins + offset + chunkSize / 2;
				var dividedBounds = new BBox( chunkMins.WithZ( totalMins.z ), chunkMaxs.WithZ( totalMaxs.z ) );

				tasks.Add( GameTask.RunInThreadAsync( () => createCells( dividedBounds, printInfo ) ) );
			}
		}

		await GameTask.WhenAll( tasks );

		foreach ( var task in tasks )
			foreach ( var cell in task.Result )
				AddCell( cell );
	}

	/// <summary>Create cells in that local bbox (Doesn't add them)</summary>
	private List<Cell> createCells( BBox bounds, bool printInfo = true )
	{
		var generatedCells = new List<Cell>();

		var minimumGrid = bounds.Mins.ToIntVector2( CellSize );
		var maximumGrid = bounds.Maxs.ToIntVector2( CellSize );
		var startingColumn = minimumGrid.y - MinimumColumn;
		var totalColumns = maximumGrid.y - minimumGrid.y;
		var endingColumn = startingColumn + totalColumns;
		var startingRow = minimumGrid.x - MinimumRow;
		var totalRows = maximumGrid.x - minimumGrid.x;
		var endingRow = startingRow + totalRows;

		for ( int column = startingColumn; column < endingColumn; column++ )
		{
			for ( int row = startingRow; row < endingRow; row++ )
			{
				var startPosition = WorldBounds.Mins.WithZ( WorldBounds.Maxs.z ) + new Vector3( row * CellSize + CellSize / 2f, column * CellSize + CellSize / 2f, Tolerance * 2f ) * AxisRotation;
				var endPosition = WorldBounds.Mins + new Vector3( row * CellSize + CellSize / 2f, column * CellSize + CellSize / 2f, -Tolerance ) * AxisRotation;
				var checkBBox = new BBox( new Vector3( -CellSize / 2f + Tolerance, -CellSize / 2f + Tolerance, 0f ), new Vector3( CellSize / 2f - Tolerance, CellSize / 2f - Tolerance, 0.001f ) );
				var positionTrace = Scene.Trace.Box( checkBBox, startPosition, endPosition )
					.WithGridSettings( Settings );

				var positionResult = positionTrace.Run();

				while ( positionResult.Hit && startPosition.z >= endPosition.z )
				{
					DebugCastsHit++;
					if ( IsInsideBounds( positionResult.HitPosition ) )
					{
						if ( !CylinderShaped || IsInsideCylinder( positionResult.HitPosition ) )
						{
							var angle = Vector3.GetAngle( Vector3.Up, positionResult.Normal );
							if ( angle <= StandableAngle )
							{
								var newCell = Cell.TryCreate( this, positionResult.HitPosition );

								if ( newCell != null )
									generatedCells.Add( newCell );
							}
							else
							{
								DebugAngleRejected++;
							}
						}
					}
					else
					{
						DebugOutOfBounds++;
					}

					startPosition = positionResult.HitPosition + Vector3.Down * HeightClearance;

					// Scene-System port of Sandbox.Trace.TestPoint: a zero-length sphere trace reports
					// StartedSolid when the point is inside geometry. Step down until we're clear.
					while ( Scene.Trace.Sphere( CellSize / 2f - Tolerance, startPosition, startPosition ).Run().StartedSolid )
						startPosition += Vector3.Down * HeightClearance;

					positionTrace = Scene.Trace.Box( checkBBox, startPosition, endPosition )
						.WithGridSettings( Settings );

					positionResult = positionTrace.Run();
				}
			}
		}

		return generatedCells;
	}

	public IEnumerable<Cell> CellsWithTag( string tag ) => AllCells.Where( cell => cell.Tags.Has( tag ) );
	public IEnumerable<Cell> CellsWithTags( params string[] tags ) => AllCells.Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithTags( List<string> tags ) => AllCells.Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithTag( BBox bounds, string tag ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tag ) );
	public IEnumerable<Cell> CellsWithTags( BBox bounds, params string[] tags ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithTags( BBox bounds, List<string> tags ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithConnection( string movementTag ) => AllCells.Where( cell => cell.GetConnections( movementTag ).Count() > 0 );
	public IEnumerable<Cell> CellsWithConnection( BBox bounds, string movementTag ) => GetCellsInBBox( bounds ).Where( cell => cell.GetConnections( movementTag ).Count() > 0 );

	public void CheckOccupancy( string tag )
	{
		foreach ( var cell in AllCells )
			cell.Occupied = cell.TestForOccupancy( tag );
	}
}
rue.house / Grid/GridSettings.cs
Game game
namespace GridAStar;

// Set STEP_SIZE or WIDTH_CLEARANCE to 0 to disable them (faster grid generation)
public static partial class GridSettings
{
	public const float DEFAULT_STANDABLE_ANGLE = 40f;   // How steep the terrain can be on a cell before it gets discarded
	public const float DEFAULT_STEP_SIZE = 12f;         // How big steps can be on a cell before it gets discarded
	public const float DEFAULT_CELL_SIZE = 16f;         // How large each cell will be in hammer units
	public const float DEFAULT_HEIGHT_CLEARANCE = 72f;  // How much vertical space there should be
	public const float DEFAULT_WIDTH_CLEARANCE = 24f;   // How much horizontal space there should be
	public const float DEFAULT_DROP_HEIGHT = 400f;      // How high you can drop down from
	public const bool DEFAULT_GRID_PERFECT = false;     // For grid-perfect terrain, if true it will not be checking for steps, so use ramps instead
	public const bool DEFAULT_STATIC_ONLY = true;       // Will it only hit world and static or also dynamic
}
rue.house / Grid/TraceExtensions.cs
Game game
using Sandbox;

namespace GridAStar;

public static partial class TraceExtensions
{
	/// <summary>
	/// Apply a grid's generation filters to a scene trace. Scene-System port: legacy <c>StaticOnly()</c>
	/// becomes <c>IgnoreDynamic()</c>. Tag filters are only applied when non-empty (an empty
	/// <c>WithAllTags</c> would otherwise filter against nothing).
	/// </summary>
	public static SceneTrace WithGridSettings( this SceneTrace self, GridBuilder settings )
	{
		if ( settings.StaticOnly )
			self = self.IgnoreDynamic();

		// ANY of the include tags (not all) - the floor may be tagged "world" while props are "solid", and
		// requiring both would match nothing. Legacy used WithAllTags but its maps used a single floor tag.
		if ( settings.TagsToInclude.Count > 0 )
			self = self.WithAnyTags( settings.TagsToInclude.ToArray() );

		if ( settings.TagsToExclude.Count > 0 )
			self = self.WithoutTags( settings.TagsToExclude.ToArray() );

		return self;
	}
}
Debug: View Raw JSON Response
{
    "TotalCount": 154,
    "Files": [
        {
            "Ident": "rue.house",
            "Path": "Game/MansionGame.Music.cs",
            "FileName": "MansionGame.Music.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using Sandbox;\n\nnamespace BrickJam;\n\npublic sealed partial class MansionGame\n{\n\t/// <summary>How fast the music fades in/out (per second). Legacy faded on the server tick.</summary>\n\tpublic float MusicVolumeChangeRate => 0.5f;\n\n\t/// <summary>Target music volume - background level, the tracks are mastered loud.</summary>\n\tpublic float MusicVolume => 0.15f;\n\n\tprivate SoundHandle musicHandle;\n\tprivate LevelType musicLevel = LevelType.None;\n\tprivate float musicVolume;\n\n\t/// <summary>\n\t/// Client-side music orchestration (every client, host included - music is local audio). Scene-System\n\t/// port of the legacy host-side <c>ProcessMusic</c>: drive the track from the replicated\n\t/// <see cref=\"CurrentLevelType\"/> and crossfade when the level changes.\n\t/// </summary>\n\tprotected override void OnUpdate()\n\t{\n\t\tvar track = Level.GetMusic( CurrentLevelType );\n\n\t\tif ( CurrentLevelType != musicLevel )\n\t\t{\n\t\t\t// Level changed: fade the old track out, then swap once it's silent.\n\t\t\tmusicVolume = musicVolume.LerpTo( 0f, MusicVolumeChangeRate * Time.Delta );\n\n\t\t\tif ( musicVolume <= 0.01f )\n\t\t\t{\n\t\t\t\tmusicHandle?.Stop();\n\t\t\t\tmusicHandle = null;\n\t\t\t\tmusicLevel = CurrentLevelType;\n\t\t\t\tmusicVolume = 0f;\n\t\t\t}\n\n\t\t\tApplyMusicVolume();\n\t\t\treturn;\n\t\t}\n\n\t\t// Same level: keep the track playing (restart if the asset isn't looped) and fade toward target.\n\t\tif ( !string.IsNullOrEmpty( track ) )\n\t\t{\n\t\t\tif ( musicHandle is null || musicHandle.IsStopped )\n\t\t\t{\n\t\t\t\tmusicHandle = Sound.Play( track );\n\t\t\t\tif ( musicHandle is not null )\n\t\t\t\t\tmusicHandle.Volume = musicVolume; // start at the current (faded) level, not full blast\n\t\t\t}\n\n\t\t\tmusicVolume = musicVolume.LerpTo( MusicVolume, MusicVolumeChangeRate * Time.Delta );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tmusicVolume = musicVolume.LerpTo( 0f, MusicVolumeChangeRate * Time.Delta );\n\t\t\tif ( musicVolume <= 0.01f && musicHandle is not null )\n\t\t\t{\n\t\t\t\tmusicHandle.Stop();\n\t\t\t\tmusicHandle = null;\n\t\t\t}\n\t\t}\n\n\t\tApplyMusicVolume();\n\t}\n\n\tprivate void ApplyMusicVolume()\n\t{\n\t\tif ( musicHandle is not null && !musicHandle.IsStopped )\n\t\t\tmusicHandle.Volume = musicVolume;\n\t}\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Grid/AStarPathBuilder.cs",
            "FileName": "AStarPathBuilder.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Sandbox;\n\nnamespace GridAStar;\n\npublic struct AStarPathBuilder\n{\n\tpublic Grid Grid { get; private set; } = null;\n\tpublic List<string> TagsToExclude { get; private set; } = new() { \"occupied\" };\n\tpublic bool HasTagsToExlude => TagsToExclude.Count() > 0;\n\tpublic bool HasOccupiedTagToExclude => HasTagsToExlude ? TagsToExclude.Contains( \"occupied\" ) : false;\n\tpublic List<string> TagsToInclude { get; private set; } = new();\n\tpublic bool HasTagsToInclude => TagsToInclude.Count() > 0;\n\tpublic Dictionary<string, float> TagsToAvoid { get; private set; } = new();\n\tpublic bool HasTagsToAvoid => TagsToAvoid.Count() > 0;\n\tpublic bool AcceptsPartial { get; private set; } = false;\n\tpublic float MaxCheckDistance { get; private set; } = float.PositiveInfinity;\n\tpublic float MaxDropHeight { get; private set; } = GridSettings.DEFAULT_DROP_HEIGHT;\n\tpublic Component PathCreator { get; private set; } = null;\n\tpublic bool HasPathCreator => PathCreator != null;\n\n\tpublic AStarPathBuilder() { }\n\tpublic AStarPathBuilder( Grid grid ) : this()\n\t{\n\t\tGrid = grid;\n\t}\n\n\tpublic static AStarPathBuilder From( Grid grid ) => new AStarPathBuilder( grid );\n\n\tpublic AStarPathBuilder WithTags( params string[] tags )\n\t{\n\t\tforeach ( var tag in tags )\n\t\t{\n\t\t\tif ( !TagsToInclude.Contains( tag ) )\n\t\t\t\tTagsToInclude.Add( tag );\n\t\t\tif ( TagsToExclude.Contains( tag ) )\n\t\t\t\tTagsToExclude.Remove( tag );\n\t\t}\n\t\treturn this;\n\t}\n\n\tpublic AStarPathBuilder WithoutTags( params string[] tags )\n\t{\n\t\tforeach ( var tag in tags )\n\t\t{\n\t\t\tif ( !TagsToExclude.Contains( tag ) )\n\t\t\t\tTagsToExclude.Add( tag );\n\t\t\tif ( TagsToInclude.Contains( tag ) )\n\t\t\t\tTagsToInclude.Remove( tag );\n\t\t}\n\t\treturn this;\n\t}\n\n\t/// <summary>\n\t/// Which tags to avoid, when found it will add the malus to its total cost.\n\t/// </summary>\n\tpublic AStarPathBuilder AvoidTag( string tag, float malus )\n\t{\n\t\tmalus = Math.Abs( malus );\n\n\t\tif ( !TagsToAvoid.ContainsKey( tag ) )\n\t\t\tTagsToAvoid.Add( tag, malus );\n\t\telse\n\t\t\tTagsToAvoid[tag] = malus;\n\n\t\treturn this;\n\t}\n\n\tpublic AStarPathBuilder WithMaxDistance( float maxDistance )\n\t{\n\t\tMaxCheckDistance = Math.Max( 0f, maxDistance );\n\t\treturn this;\n\t}\n\n\tpublic AStarPathBuilder WithMaxDropHeight( float maxDropHeight )\n\t{\n\t\tMaxDropHeight = Math.Min( Grid.MaxDropHeight, maxDropHeight );\n\t\treturn this;\n\t}\n\n\tpublic AStarPathBuilder WithPartialEnabled()\n\t{\n\t\tAcceptsPartial = true;\n\t\treturn this;\n\t}\n\n\tpublic AStarPathBuilder WithPathCreator( Component pathCreator )\n\t{\n\t\tPathCreator = pathCreator;\n\t\treturn this;\n\t}\n\n\tpublic AStarPath Run( Cell startingCell, Cell targetCell, bool reversed = false, bool withCellConnections = true )\n\t{\n\t\tif ( startingCell is null || targetCell is null || startingCell == targetCell ) return AStarPath.Empty();\n\n\t\treturn AStarPath.From( this, Grid.ComputePathInternal( this, startingCell, targetCell, CancellationToken.None, reversed, withCellConnections ) );\n\t}\n\tpublic AStarPath Run( Vector3 startingPosition, Cell targetCell, bool reversed = false, bool withCellConnections = true ) => Run( Grid.GetCell( startingPosition ), targetCell, reversed, withCellConnections );\n\tpublic AStarPath Run( Cell startingCell, Vector3 targetPosition, bool reversed = false, bool withCellConnections = true ) => Run( startingCell, Grid.GetCell( targetPosition ), reversed, withCellConnections );\n\tpublic AStarPath Run( Vector3 startingPosition, Vector3 targetPosition, bool reversed = false, bool withCellConnections = true ) => Run( Grid.GetCell( startingPosition ), Grid.GetCell( targetPosition ), reversed, withCellConnections );\n\n\tinternal AStarPath Run( Cell startingCell, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true )\n\t{\n\t\tif ( startingCell is null || targetCell is null || startingCell == targetCell ) return AStarPath.Empty();\n\n\t\treturn AStarPath.From( this, Grid.ComputePathInternal( this, startingCell, targetCell, token, reversed, withCellConnections ) );\n\t}\n\n\tpublic async Task<AStarPath> RunAsync( Cell startingCell, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true )\n\t{\n\t\tvar builder = this;\n\n\t\treturn await GameTask.RunInThreadAsync( () => builder.Run( startingCell, targetCell, token, reversed, withCellConnections ) );\n\t}\n\tpublic async Task<AStarPath> RunAsync( Vector3 startingPosition, Cell targetCell, CancellationToken token, bool reversed = false, bool withCellConnections = true ) => await RunAsync( Grid.GetCell( startingPosition ), targetCell, token, reversed, withCellConnections );\n\tpublic async Task<AStarPath> RunAsync( Cell startingCell, Vector3 targetPosition, CancellationToken token, bool reversed = false, bool withCellConnections = true ) => await RunAsync( startingCell, Grid.GetCell( targetPosition ), token, reversed, withCellConnections );\n\tpublic async Task<AStarPath> RunAsync( Vector3 startingPosition, Vector3 targetPosition, CancellationToken token, bool reversed = false, bool withCellConnections = true ) => await RunAsync( Grid.GetCell( startingPosition ), Grid.GetCell( targetPosition ), token, reversed, withCellConnections );\n\n\tpublic async Task<AStarPath> RunInParallel( Cell startingCell, Cell targetCell, CancellationTokenSource tokenSource, bool withCellConnections = true )\n\t{\n\t\tif ( startingCell is null || targetCell is null || startingCell == targetCell ) return AStarPath.Empty();\n\n\t\tvar fromTo = RunAsync( startingCell, targetCell, tokenSource.Token, false, withCellConnections );\n\t\tvar toFrom = RunAsync( targetCell, startingCell, tokenSource.Token, true, false ); // You can't reverse some cell connections, like dropping down\n\n\t\tvar pathResult = await GameTask.WhenAny( fromTo, toFrom ).Result;\n\n\t\t// Cancel the other task that hasn't finished yet.\n\t\ttokenSource.Cancel();\n\n\t\treturn pathResult;\n\t}\n\tpublic async Task<AStarPath> RunInParallel( Vector3 startingPosition, Cell targetCell, CancellationTokenSource tokenSource, bool withCellConnections = true ) => await RunInParallel( Grid.GetCell( startingPosition ), targetCell, tokenSource, withCellConnections );\n\tpublic async Task<AStarPath> RunInParallel( Cell startingCell, Vector3 targetPosition, CancellationTokenSource tokenSource, bool withCellConnections = true ) => await RunInParallel( startingCell, Grid.GetCell( targetPosition ), tokenSource, withCellConnections );\n\tpublic async Task<AStarPath> RunInParallel( Vector3 startingPosition, Vector3 targetPosition, CancellationTokenSource tokenSource, bool withCellConnections = true ) => await RunInParallel( Grid.GetCell( startingPosition ), Grid.GetCell( targetPosition ), tokenSource, withCellConnections );\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Grid/IntVector2.cs",
            "FileName": "IntVector2.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System;\nusing Sandbox;\n\nnamespace GridAStar;\n\n/// <summary>\n/// Like a Vector2, but with integers instead.\n/// </summary>\npublic struct IntVector2 : IEquatable<IntVector2>\n{\n\tpublic int x { get; set; }\n\tpublic int y { get; set; }\n\n\tpublic int this[int index]\n\t{\n\t\tget\n\t\t{\n\t\t\tint result = index switch\n\t\t\t{\n\t\t\t\t0 => x,\n\t\t\t\t1 => y,\n\t\t\t\t_ => throw new IndexOutOfRangeException(),\n\t\t\t};\n\n\t\t\treturn result;\n\t\t}\n\t\tset\n\t\t{\n\t\t\tswitch ( index )\n\t\t\t{\n\t\t\t\tcase 0:\n\t\t\t\t\tx = value;\n\t\t\t\t\tbreak;\n\t\t\t\tcase 1:\n\t\t\t\t\ty = value;\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\tpublic IntVector2( int x, int y )\n\t{\n\t\tthis.x = x;\n\t\tthis.y = y;\n\t}\n\n\tpublic IntVector2 WithX( int x ) => new IntVector2( x, this.y );\n\tpublic IntVector2 WithY( int y ) => new IntVector2( this.x, y );\n\tpublic Vector2 ToVector2() => new Vector2( x, y );\n\tpublic float DistanceSquared( IntVector2 other ) => ToVector2().DistanceSquared( other.ToVector2() );\n\tpublic override string ToString() => $\"{x},{y}\";\n\tpublic override bool Equals( object obj ) => obj is IntVector2 other && Equals( other );\n\tpublic bool Equals( IntVector2 other ) => x == other.x && y == other.y;\n\tpublic override int GetHashCode() => HashCode.Combine( x, y );\n\n\tpublic static bool operator ==( IntVector2 left, IntVector2 right ) => left.Equals( right );\n\tpublic static bool operator !=( IntVector2 left, IntVector2 right ) => !(left == right);\n\tpublic static IntVector2 operator +( IntVector2 a, IntVector2 b ) => new IntVector2( a.x + b.x, a.y + b.y );\n\tpublic static IntVector2 operator -( IntVector2 a, IntVector2 b ) => new IntVector2( a.x - b.x, a.y - b.y );\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Levels/Level.cs",
            "FileName": "Level.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing System.Collections.Generic;\nusing Sandbox;\n\nnamespace BrickJam;\n\n/// <summary>\n/// Level orchestration. The legacy <c>Level</c> was an <c>Entity</c> (for replication); in the Scene\n/// System it is a plain server-side object owned by <see cref=\"MansionGame\"/>. The single replicated\n/// fact clients need \u2014 the current level type \u2014 lives on the manager as\n/// <see cref=\"MansionGame.CurrentLevelType\"/>.\n///\n/// DEFERRED: grid generation (<c>GenerateGrid</c>, needs the grid package), music\n/// (<c>ProcessMusic</c>), and the black-screen transition + event-log messages (UI system).\n/// </summary>\npublic abstract partial class Level\n{\n\tpublic abstract LevelType Type { get; }\n\tpublic string Music => GetMusic( Type );\n\tpublic virtual BBox WorldBox => new( new Vector3( -100000f ), new Vector3( 100000f ) );\n\n\t/// <summary>\n\t/// Music track per level. Single source of truth so the client-side music player\n\t/// (<see cref=\"MansionGame\"/>) can resolve a track from the replicated <see cref=\"LevelType\"/>\n\t/// without a host-side <see cref=\"Level\"/> instance.\n\t/// </summary>\n\tpublic static string GetMusic( LevelType type ) => type switch\n\t{\n\t\tLevelType.Shop => \"sounds/music/scary_quest_at_midnight.sound\",\n\t\tLevelType.Mansion => \"sounds/music/looming_trees_in_eerie_woods.sound\",\n\t\tLevelType.Dungeon => \"sounds/music/malevolent_sightings_in_the_room.sound\",\n\t\tLevelType.Bathrooms => \"sounds/music/depths_and_terror.sound\",\n\t\t_ => null,\n\t};\n\n\tpublic LegacyUsableComponent Exit { get; set; }\n\tpublic List<NPC> Monsters { get; } = new();\n\tpublic TimeSince SinceStarted { get; set; }\n\n\tpublic static bool GameIsEnding { get; set; }\n\n\tprotected Scene Scene => MansionGame.Instance.Scene;\n\n\tpublic virtual void Compute()\n\t{\n\t\tvar players = Scene.GetAllComponents<Player>().ToList();\n\t\tif ( players.Count > 0 && players.All( x => !x.IsAlive ) && !GameIsEnding )\n\t\t{\n\t\t\tMansionGame.RestartGame();\n\t\t\tGameIsEnding = true;\n\t\t\t// TODO (UI): Eventlog \"Looks like everyone died, better luck next time!\"\n\t\t}\n\t}\n\n\tprotected void RespawnAll() => MansionGame.Instance.RespawnAll();\n\n\tpublic virtual async Task Start()\n\t{\n\t\tawait GameTask.Yield();\n\n\t\tRespawnAll();\n\n\t\t// Companion spawning for players who bought the upgrade.\n\t\tforeach ( var player in Scene.GetAllComponents<Player>().ToList() )\n\t\t{\n\t\t\tif ( player.HasUpgrade( \"Cartoony Sidekick\" ) )\n\t\t\t{\n\t\t\t\tvar doob = NPC.Create<Doob>( player.WorldPosition, player.WorldRotation );\n\t\t\t\tdoob.Owner = player;\n\t\t\t\tplayer.Doob = doob;\n\n\t\t\t\t// Who's a Good Boy?: Doob summoned to protect this player.\n\t\t\t\tplayer.TrackAchievement( GameStats.AchGoodBoy );\n\t\t\t}\n\t\t}\n\n\t\tExit?.GameObject.Destroy();\n\n\t\tif ( Type == LevelType.Bathrooms )\n\t\t{\n\t\t\tvar finalDoors = Scene.GetAllComponents<ValidFinalDoorPosition>().ToList();\n\t\t\tvar spot = MansionGame.Random.FromList( finalDoors, null );\n\t\t\tif ( spot is not null )\n\t\t\t\tExit = FinalDoor.Create( spot.WorldPosition, spot.WorldRotation );\n\t\t}\n\t\telse\n\t\t{\n\t\t\tvar trapdoors = Scene.GetAllComponents<ValidTrapdoorPosition>().Where( x => x.LevelType == Type ).ToList();\n\t\t\tvar spot = MansionGame.Random.FromList( trapdoors, null );\n\t\t\tif ( spot is not null )\n\t\t\t\tExit = Trapdoor.Create( spot.WorldPosition, spot.WorldRotation );\n\t\t}\n\n\t\tforeach ( var spawner in Scene.GetAllComponents<LootSpawner>().Where( x => x.LevelType == Type ).ToList() )\n\t\t\tspawner.SpawnLoot();\n\n\t\tforeach ( var door in Scene.GetAllComponents<Door>().Where( x => x.LevelType == Type ).ToList() )\n\t\t\tdoor.Close();\n\n\t\tawait GameTask.DelayRealtimeSeconds( 1f );\n\n\t\tawait GenerateGrid();\n\n\t\tGameIsEnding = false;\n\t\tSinceStarted = 0f; // reset the level clock (used by the Slipped on a Soap achievement)\n\t\tMansionGame.Instance.TimerStart();\n\t}\n\n\tpublic virtual async Task End()\n\t{\n\t\tawait GameTask.Yield();\n\n\t\tMansionGame.Instance?.ShowBlackScreen( 2f, 1f, 1f );\n\n\t\tExit?.GameObject.Destroy();\n\t\tExit = null;\n\n\t\tforeach ( var monster in Monsters.ToList() )\n\t\t\tRemoveMonster( monster );\n\n\t\tforeach ( var spawner in Scene.GetAllComponents<LootSpawner>().ToList() )\n\t\t\tspawner.DeleteLoot();\n\n\t\tforeach ( var door in Scene.GetAllComponents<Door>().Where( x => WorldBox.Contains( x.WorldPosition ) ).ToList() )\n\t\t\tdoor.Close();\n\n\t\tforeach ( var loot in Scene.GetAllComponents<Loot>().ToList() )\n\t\t\tloot.GameObject.Destroy();\n\n\t\tMansionGame.Instance.TimerStop();\n\n\t\tforeach ( var doob in Scene.GetAllComponents<Doob>().ToList() )\n\t\t{\n\t\t\tif ( doob.Owner.IsValid() )\n\t\t\t\tdoob.Owner.Doob = null;\n\n\t\t\tdoob.GameObject.Destroy();\n\t\t}\n\t}\n\n\tpublic virtual void RegisterMonster( NPC monster ) => Monsters.Add( monster );\n\n\tpublic virtual void RemoveMonster( NPC monster )\n\t{\n\t\tMonsters.Remove( monster );\n\t\tif ( monster.IsValid() )\n\t\t\tmonster.GameObject.Destroy();\n\t}\n\n\tpublic static Type GetClrType( LevelType type ) => type switch\n\t{\n\t\tLevelType.Shop => typeof( ShopLevel ),\n\t\tLevelType.Mansion => typeof( MansionLevel ),\n\t\tLevelType.Dungeon => typeof( DungeonLevel ),\n\t\tLevelType.Bathrooms => typeof( BathroomsLevel ),\n\t\t_ => null,\n\t};\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Levels/ShopLevel.cs",
            "FileName": "ShopLevel.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System.Linq;\nusing System.Threading.Tasks;\nusing Sandbox;\n\nnamespace BrickJam;\n\npublic sealed class ShopLevel : Level\n{\n\tpublic override LevelType Type => LevelType.Shop;\n\n\tpublic override async Task Start()\n\t{\n\t\t// Shop is a safe hub - no base.Start() (no exit/monsters/loot/grid).\n\t\tawait GameTask.Yield();\n\n\t\tRespawnAll();\n\t\tMansionGame.Instance.TimerStop();\n\t}\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "NPC/Specter.cs",
            "FileName": "Specter.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Sandbox;\nusing GridAStar;\n\nnamespace BrickJam;\n\n/// <summary>\n/// Specter monster - sinks into the floor and rises elsewhere. Scene-System port of legacy <c>Specter</c>,\n/// now on grid A*: it chases directly, and on a long idle teleports to a random grid cell (sink \u2192 reposition\n/// \u2192 rise). The lamp light flicker (legacy CapsuleLightEntity) is deferred to the effects system.\n/// </summary>\n[Title( \"Specter\" )]\n[Category( \"NPC\" )]\npublic sealed partial class Specter : NPC\n{\n\tpublic override string ModelPath { get; set; } = \"models/specter/specter.vmdl\";\n\tpublic override float WalkSpeed { get; set; } = 120f;\n\tpublic override float RunSpeed { get; set; } = 320f;\n\tpublic override float MaxVisionAngle { get; set; } = 240f;\n\tpublic override float MaxVisionRange { get; set; } = 1000f;\n\tpublic override float MaxVisionRangeWhenChasing { get; set; } = 1000f;\n\tpublic override float MaxVisionAngleWhenChasing { get; set; } = 180f;\n\tpublic override float MaxRememberTime { get; set; } = 3f;\n\tpublic override string IdleSound => \"sounds/specter/spectermoan.sound\";\n\tpublic override float IdleVolume => 1.5f;\n\tpublic override string AttackSound => \"sounds/specter/spectermoan.sound\";\n\tpublic override float AttackVolume => 2f;\n\n\tpublic float TimeToTeleport => 2f;\n\tpublic bool IsLowering => LastTeleport <= TimeToTeleport / 2f + 0.5f;\n\tpublic bool IsRising => LastTeleport <= TimeToTeleport + 1f && LastTeleport > TimeToTeleport / 2f + 0.5f;\n\tpublic bool IsTeleporting => IsLowering || IsRising;\n\n\t[Sync] public TimeSince LastTeleport { get; set; } = 999f;\n\n\tprivate static readonly Color LampColor = new( 1f, 0.55f, 0.2f );\n\tprivate PointLight lamp;\n\n\tprotected override void OnStart()\n\t{\n\t\tbase.OnStart();\n\n\t\t// Atmospheric lamp light (legacy CapsuleLightEntity). Purely visual, so each client builds its OWN\n\t\t// non-networked light and flickers it locally in OnUpdate - NPC logic (Think) is host-only and the\n\t\t// flicker shouldn't depend on replicated state.\n\t\tvar lampGo = new GameObject( true, \"Lamp\" ) { Parent = GameObject };\n\t\tlampGo.LocalPosition = Vector3.Up * 40f;\n\t\tlamp = lampGo.Components.Create<PointLight>();\n\t\tlamp.LightColor = LampColor;\n\t\tlamp.Radius = 350f;\n\t\tlamp.Shadows = false; // atmospheric glow - skip the (6-face) shadow pass for this point light\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !lamp.IsValid() )\n\t\t\treturn;\n\n\t\t// Eerie flicker; fade the lamp out while the specter is sunk into the floor (teleporting).\n\t\tvar t = Time.Now;\n\t\tvar flicker = 0.7f + 0.18f * MathF.Sin( t * 27f ) + 0.12f * MathF.Sin( t * 11.3f );\n\t\tvar visible = IsTeleporting ? 0f : 1f;\n\n\t\tlamp.LightColor = LampColor * (flicker * visible * 4f);\n\t}\n\n\tpublic override void ComputeIdleAndSeek()\n\t{\n\t\t// While sinking/rising we don't make new decisions; ComputeMotion drives the vertical move.\n\t\tif ( IsTeleporting )\n\t\t\treturn;\n\n\t\tif ( InVision.Count > 0 )\n\t\t{\n\t\t\tTarget = InVision.OrderBy( x => x.Key.WorldPosition.Distance( WorldPosition ) ).FirstOrDefault().Key;\n\n\t\t\tif ( Target is Player player && player.Doob.IsValid() )\n\t\t\t\tTarget = player.Doob;\n\n\t\t\tLastTarget = Target;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tTarget = null;\n\n\t\t\tif ( !IsFollowingPath && nextIdle && CurrentGrid is not null )\n\t\t\t{\n\t\t\t\tvar isLongIdle = MansionGame.Random.NextSingle() <= 0.2f;\n\t\t\t\tvar allCells = CurrentGrid.AllCells.ToList();\n\n\t\t\t\tCell chosen = null;\n\t\t\t\tfor ( var tried = 0; tried < 20 && allCells.Count > 0; tried++ )\n\t\t\t\t{\n\t\t\t\t\tvar candidate = MansionGame.Random.FromList( allCells, null );\n\t\t\t\t\tif ( candidate is null )\n\t\t\t\t\t\tbreak;\n\n\t\t\t\t\tvar dist = candidate.Position.Distance( WorldPosition );\n\t\t\t\t\tif ( isLongIdle ? dist >= 1000f : (dist >= 400f && dist <= 1000f) )\n\t\t\t\t\t{\n\t\t\t\t\t\tchosen = candidate;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( chosen != null )\n\t\t\t\t{\n\t\t\t\t\tif ( isLongIdle )\n\t\t\t\t\t\tTeleport( chosen.Position );\n\t\t\t\t\telse\n\t\t\t\t\t\tNavigateTo( chosen );\n\t\t\t\t}\n\n\t\t\t\tnextIdle = MansionGame.Random.NextSingle() * 1f + 1f;\n\t\t\t\tLastTarget = null;\n\t\t\t}\n\t\t}\n\n\t\tif ( Target.IsValid() && Target.WorldPosition.Distance( WorldPosition ) <= KillRange )\n\t\t{\n\t\t\tif ( Target is Player p )\n\t\t\t\t_ = CatchPlayer( p );\n\t\t\telse if ( Target is Doob d )\n\t\t\t\t_ = CatchDoob( d );\n\t\t}\n\n\t\tif ( nextIdleSound && !string.IsNullOrEmpty( IdleSound ) )\n\t\t{\n\t\t\tSoundExtensions.BroadcastPlay( IdleSound, WorldPosition, IdleVolume );\n\t\t\tnextIdleSound = MansionGame.Random.NextSingle() * 4f + 4f;\n\t\t}\n\t}\n\n\tpublic override void ComputeMotion()\n\t{\n\t\tif ( !IsTeleporting )\n\t\t{\n\t\t\tbase.ComputeMotion();\n\t\t\treturn;\n\t\t}\n\n\t\t// Sink into / rise out of the floor under manual control (off the grid).\n\t\tif ( IsLowering )\n\t\t\tWorldPosition += Vector3.Down * CollisionHeight * Time.Delta / (TimeToTeleport / 2f);\n\t\telse if ( IsRising )\n\t\t\tWorldPosition += Vector3.Up * CollisionHeight * Time.Delta / (TimeToTeleport / 2f);\n\t}\n\n\tpublic async void Teleport( Vector3 position )\n\t{\n\t\tLastTeleport = 0;\n\t\tMansionGame.Instance?.PlayEffect( \"prefabs/particles/specter_teleport.prefab\", WorldPosition, Rotation.Identity );\n\n\t\tawait Task.DelayRealtimeSeconds( TimeToTeleport * 0.5f + 0.5f );\n\n\t\tWorldPosition = position + Vector3.Down * CollisionHeight;\n\t}\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "UI/GroundLootPanel.razor",
            "FileName": "GroundLootPanel.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "@using System\n@using Sandbox\n@using Sandbox.UI\n@namespace BrickJam.UI\n@inherits PanelComponent\n\n<root>\n\t@if ( Loot.IsValid() )\n\t{\n\t\t<div class=\"container\">\n\t\t\t<span>@Loot.FullName</span>\n\t\t\t<div class=\"row\">\n\t\t\t\t<span class=\"currency\">$</span><span>@($\"{Loot.MonetaryValue:n0}\")</span>\n\t\t\t</div>\n\t\t</div>\n\t}\n</root>\n\n@code {\n\tprivate Loot loot;\n\tprivate Loot Loot => loot ??= GetComponentInParent<Loot>();\n\n\tprotected override int BuildHash() => HashCode.Combine( Loot?.FullName, Loot?.MonetaryValue ?? 0 );\n}\n\n<style>\n\tGroundLootPanel {\n\t\ttransition: transform 0.5s ease-in-out;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\n\t\t.container {\n\t\t\tpadding: 10px;\n\t\t\tpadding-right: 30px;\n\t\t\tpadding-left: 30px;\n\t\t\tfont-size: 32px;\n\t\t\talign-items: center;\n\t\t\tfont-family: \"alagard\";\n\t\t\tflex-direction: column;\n\t\t\tcolor: white;\n\t\t\ttext-shadow: 3px 3px 0px black;\n\t\t\tbackground: linear-gradient(to left, rgba(black, 0) 0%, rgba(black, 0.5) 20%, rgba(black, 0.5) 80%, rgba(black, 0) 100%);\n\n\t\t\t.currency {\n\t\t\t\tpadding-right: 4px;\n\t\t\t\tfont-size: 22px;\n\t\t\t\ttop: 5px;\n\t\t\t\tcolor: rgba(50, 205, 50, 1);\n\t\t\t}\n\t\t}\n\n\t\t&:outro {\n\t\t\ttransform: scale(0);\n\t\t}\n\n\t\t&:intro {\n\t\t\ttransform: scale(0);\n\t\t}\n\t}\n</style>\n"
        },
        {
            "Ident": "rue.house",
            "Path": "UI/Hud.razor",
            "FileName": "Hud.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "@using System\n@using Sandbox\n@using Sandbox.UI\n@namespace BrickJam.UI\n@inherits PanelComponent\n\n<root>\n\t@{\n\t\tvar player = Player.Local;\n\n\t\tvar isSpectator = player?.Spectating ?? false;\n\t\tvar isPlayer = player?.IsValid() ?? false;\n\t}\n\n\t<MoneyCounter />\n\t<Eventlog />\n\t<PlayerCounter />\n\t<StunIndicator />\n\t<QuickPing />\n\t<SubtitlesList />\n\t<Lockpicker />\n\t<BlackScreen />\n\t\n\t<div class=\"input-hints\">\n\t\t@if ( isSpectator )\n\t\t{\n\t\t\t<div class=\"hint\"><inputglyph action=\"StopFollowing\"/>Stop following</div>\n\t\t\t<div class=\"hint\"><inputglyph action=\"FollowNext\"/>Next player</div>\n\t\t\t<div class=\"hint\"><inputglyph action=\"FollowPrevious\"/>Previous player</div>\n\t\t}\n\t\telse\n\t\t{\n\t\t\t<div class=\"hint\"><inputglyph action=\"inventory\"/>Inventory</div>\n\t\t\t<div class=\"hint\"><inputglyph action=\"chat\"/>Chat</div>\n\t\t\t<div class=\"hint\"><inputglyph action=\"ping\"/>Quick Ping</div>\n\t\t}\n\t</div>\n\n\t@if ( player.IsValid() )\n\t{\n\t\t<Inventory />\n\t\t<Shop />\n\t\t<TextInput Ghost=\"Say something...\" class=\"chat-input\" @ref=\"ChatInput\" onsubmit=@OnChatSubmit />\n\n\t\t@if ( !player.SeenTips )\n\t\t{\n\t\t\t<TutorialTips />\n\t\t}\n\t}\n\n\t@if ( player.IsValid() && player.IsAlive && !LockpickerBus.IsOpen )\n\t{\n\t\t<div class=\"crosshair\">\n\t\t\t<div class=\"mark\">+</div>\n\t\t\t<RadialProgress />\n\t\t\t<InteractionTip />\n\t\t</div>\n\t}\n\n\t@if ( player.IsValid() && !player.IsAlive )\n\t{\n\t\t<div class=\"death\">\n\t\t\t<span class=\"title\">YOU'RE DEAD!</span>\n\t\t</div>\n\t}\n</root>\n\n@code {\n\tprivate TextInput ChatInput { get; set; }\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( ChatInput is null )\n\t\t\treturn;\n\n\t\tif ( Input.Pressed( \"chat\" ) )\n\t\t{\n\t\t\tChatInput.Focus();\n\t\t\tChatInput.AddClass( \"visible\" );\n\t\t}\n\t}\n\n\tprivate void OnChatSubmit()\n\t{\n\t\tif ( ChatInput is null )\n\t\t\treturn;\n\n\t\tvar text = ChatInput.Text;\n\t\tif ( !string.IsNullOrWhiteSpace( text ) )\n\t\t\tPlayer.Local?.Say( text );\n\n\t\tChatInput.Text = \"\";\n\t\tChatInput.RemoveClass( \"visible\" );\n\t}\n\n\tprotected override int BuildHash() => HashCode.Combine(\n\t\tPlayer.Local,\n\t\tPlayer.Local?.IsAlive ?? false,\n\t\tLockpickerBus.IsOpen );\n}\n\n<style>\n\tHud {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\t\tfont-family: \"alagard\";\n\t\tjustify-content: flex-start;\n\n\t\t.crosshair {\n\t\t\tposition: absolute;\n\t\t\ttop: 0; left: 0;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tflex-direction: column;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\n\t\t\t.mark {\n\t\t\t\tcolor: white;\n\t\t\t\tfont-size: 28px;\n\t\t\t\ttext-shadow: 2px 2px 0px black;\n\t\t\t}\n\t\t}\n\n\t\t.input-hints {\n\t\t\tposition: absolute;\n\t\t\tbottom: 30px;\n\t\t\tright: 0px;\n\t\t\talign-items: flex-end;\n\t\t\tflex-direction: column;\n\t\t\tz-index: 3;\n\n\t\t\t.hint {\n\t\t\t\tcolor: white;\n\t\t\t\theight: 32px;\n\t\t\t\tjustify-content: center;\n\t\t\t\tfont-size: 32px;\n\t\t\t\tpadding-left: 50px;\n\t\t\t\tpadding-top: 10px;\n\t\t\t\tpadding-bottom: 40px;\n\t\t\t\tpadding-right: 10px;\n\t\t\t\tbackground: linear-gradient(to left, rgba(black, 0.5) 0%, rgba(black, 0.2) 75%, rgba(black, 0) 100%);\n\t\t\t\tmargin-top: 5px;\n\t\t\t\ttext-shadow: 3px 3px 0px black;\n\n\t\t\t\tInputGlyph {\n\t\t\t\t\twidth: 32px;\n\t\t\t\t\taspect-ratio: 1;\n\t\t\t\t\tmargin-right: 10px;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t.chat-input {\n\t\t\tposition: absolute;\n\t\t\twidth: 450px;\n\t\t\tmin-height: 20px;\n\t\t\tfont-size: 24px;\n\t\t\topacity: 0;\n\t\t\tpointer-events: all;\n\t\t\tbackground-color: rgba(37, 64, 98, 1);\n\t\t\tbox-shadow: 3px 3px 0px 0px rgba(17, 44, 78, 1);\n\t\t\ttransition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;\n\t\t\ttransform: translate(-50% 200px);\n\t\t\ttext-shadow: 3px 3px 0px black;\n\t\t\tleft: 50%;\n\t\t\ttop: 50%;\n\t\t\tmargin-top: 40px;\n\n\t\t\t&.visible {\n\t\t\t\topacity: 1;\n\t\t\t\ttransform: translate(-50% 0px);\n\t\t\t}\n\t\t}\n\n\t\t.death {\n\t\t\tposition: absolute;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tleft: 0px;\n\t\t\ttop: 0px;\n\t\t\tcolor: white;\n\t\t\tfont-size: 32px;\n\t\t\tz-index: 2;\n\t\t\tbackdrop-filter: grayscale(100%);\n\t\t\tjustify-content: center;\n\t\t\ttransition: opacity 1s ease-in-out;\n\t\t\topacity: 0;\n\t\t\ttext-shadow: 4px 4px 0px black;\n\n\t\t\t&.visible {\n\t\t\t\topacity: 1;\n\t\t\t}\n\n\t\t\t.container {\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\tspan {\n\t\t\t\tbackground: linear-gradient(to left, rgba(black, 0) 0%, rgba(black, 0.4) 20%, rgba(black, 0.4) 80%, rgba(black, 0) 100%);\n\t\t\t\tpadding: 10px;\n\t\t\t\tpadding-left: 40px;\n\t\t\t\tpadding-right: 40px;\n\t\t\t}\n\n\t\t\t.title {\n\t\t\t\tcolor: red;\n\t\t\t\tfont-size: 64px;\n\t\t\t\tmargin-top: 150px;\n\t\t\t\tmargin-bottom: 20px;\n\t\t\t}\n\t\t}\n\t}\n</style>\n"
        },
        {
            "Ident": "rue.house",
            "Path": "UI/Nametag.razor",
            "FileName": "Nametag.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "@using System\n@using Sandbox\n@using Sandbox.UI\n@using WorldPanel = Sandbox.WorldPanel\n@namespace BrickJam.UI\n@inherits PanelComponent\n\n<root>\n\t@if ( !IsLocal && Player.IsValid() && Player.IsAlive )\n\t{\n\t\t<div class=\"container\">\n\t\t\t<span>@Name</span>\n\t\t</div>\n\t}\n</root>\n\n@code {\n\tprivate Player player;\n\tprivate Player Player => player ??= GetComponentInParent<Player>();\n\n\t// Don't draw your own nametag in first person.\n\tprivate bool IsLocal => Player == Player.Local;\n\n\tprivate string Name => Player.IsValid() && Player.Network.Owner is { } owner ? owner.DisplayName : \"player\";\n\n\tprotected override int BuildHash() => HashCode.Combine( IsLocal, Name, Player?.IsAlive ?? false );\n}\n\n<style>\n\tNametag {\n\t\ttransition: transform 0.5s ease-in-out;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\twidth: 100%;\n\t\theight: 100%;\n\n\t\t.container {\n\t\t\tpadding: 10px;\n\t\t\tpadding-right: 30px;\n\t\t\tpadding-left: 30px;\n\t\t\tfont-size: 32px;\n\t\t\talign-items: center;\n\t\t\tfont-family: \"alagard\";\n\t\t\tflex-direction: column;\n\t\t\tcolor: white;\n\t\t\ttext-shadow: 3px 3px 0px black;\n\t\t\tbackground: linear-gradient(to left, rgba(black, 0) 0%, rgba(black, 0.5) 20%, rgba(black, 0.5) 80%, rgba(black, 0) 100%);\n\t\t}\n\n\t\t&:outro {\n\t\t\ttransform: scale(0);\n\t\t}\n\n\t\t&:intro {\n\t\t\ttransform: scale(0);\n\t\t}\n\t}\n</style>\n"
        },
        {
            "Ident": "rue.house",
            "Path": "UI/PingBus.cs",
            "FileName": "PingBus.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System;\nusing Sandbox;\n\nnamespace BrickJam.UI;\n\n/// <summary>Decouples gameplay from the Razor <c>QuickPing</c> panel (see <see cref=\"EventlogBus\"/>).</summary>\npublic static class PingBus\n{\n\tpublic enum PingType { Enemy, Exit, Loot, Other }\n\n\tpublic static event Action<Vector3, PingType> OnPing;\n\n\tpublic static void Post( Vector3 worldPosition, PingType type ) => OnPing?.Invoke( worldPosition, type );\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "UI/SubtitlesList.razor",
            "FileName": "SubtitlesList.razor",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "@using System\n@using System.Linq\n@using System.Collections.Generic\n@using Sandbox\n@using Sandbox.UI\n@namespace BrickJam.UI\n@inherits Panel\n\n<root>\n\t@foreach ( var line in lines )\n\t{\n\t\t<div class=\"subtitle\">\n\t\t\t@if ( !string.IsNullOrEmpty( line.Speaker ) )\n\t\t\t{\n\t\t\t\t<span class=\"speaker\">@line.Speaker:</span>\n\t\t\t}\n\t\t\t<span class=\"text\">@line.Text</span>\n\t\t</div>\n\t}\n</root>\n\n@code {\n\tprivate readonly List<(string Speaker, string Text, TimeUntil Expire)> lines = new();\n\n\tpublic SubtitlesList()\n\t{\n\t\tSubtitleBus.OnSubtitle += Add;\n\t}\n\n\tpublic override void OnDeleted()\n\t{\n\t\tbase.OnDeleted();\n\t\tSubtitleBus.OnSubtitle -= Add;\n\t}\n\n\tprivate void Add( string speaker, string text, float duration )\n\t{\n\t\tlines.Add( (speaker, text, duration) );\n\t\tif ( lines.Count > 3 )\n\t\t\tlines.RemoveAt( 0 );\n\t}\n\n\tpublic override void Tick()\n\t{\n\t\tlines.RemoveAll( l => l.Expire );\n\t}\n\n\tprotected override int BuildHash() => HashCode.Combine( lines.Count, lines.LastOrDefault().Text );\n}\n\n<style>\n\tSubtitlesList {\n\t\tposition: absolute;\n\t\tleft: 0; right: 0;\n\t\tbottom: 60px;\n\t\tflex-direction: column;\n\t\talign-items: center;\n\n\t\t.subtitle {\n\t\t\tflex-direction: row;\n\t\t\tpadding: 6px 16px;\n\t\t\tmargin-top: 4px;\n\t\t\tbackground-color: rgba(0,0,0,0.6);\n\t\t\tfont-size: 26px;\n\t\t\ttext-shadow: 2px 2px 0px black;\n\n\t\t\t.speaker { color: rgba(255,220,80,1); margin-right: 8px; }\n\t\t\t.text { color: white; }\n\t\t}\n\t}\n</style>\n"
        },
        {
            "Ident": "rue.house",
            "Path": "ui/controls/switchcontrol.razor.scss",
            "FileName": "switchcontrol.razor.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "\r\n.switchcontrol\r\n{\r\n    flex-direction: row;\r\n    width: 100px;\r\n    min-height: 24px;\r\n    align-items: center;\r\n    cursor: pointer;\r\n\r\n    .switch-frame\r\n    {\r\n        flex-grow: 0;\r\n        flex-shrink: 1;\r\n        width: 48px;\r\n        height: 16px;\r\n        background-color: #fff1;\r\n        margin: 0px 5px;\r\n        align-items: center;\r\n        border-radius: 100px;\r\n        transition: all 0.4s linear;\r\n\r\n        .switch-inner\r\n        {\r\n            position: relative;\r\n            flex-grow: 0;\r\n            flex-shrink: 1;\r\n            background-color: #999;\r\n            width: 25px;\r\n            height: 25px;\r\n            border-radius: 100px;\r\n            left: 20%;\r\n            transform: translateX( -50% );\r\n            transition: all 0.3s ease-out;\r\n        }\r\n    }\r\n\r\n    &.active\r\n    {\r\n        .switch-frame\r\n        {\r\n            background-color: #fffa;\r\n        }\r\n\r\n        .switch-inner\r\n        {\r\n            left: 80%;\r\n            background-color: #fff;\r\n        }\r\n    }\r\n}\r\n"
        },
        {
            "Ident": "rue.house",
            "Path": "styles/form.scss",
            "FileName": "form.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "\r\n$form-control-height: 28px !default;\r\n\r\n@import \"form/_checkbox.scss\";\r\n@import \"form/_switch.scss\";\r\n@import \"form/_dropdown.scss\";\r\n@import \"form/_coloreditor.scss\";\r\n@import \"form/_colorproperty.scss\";\r\n\r\n.form\r\n{\r\n\tflex-direction: column;\r\n\talign-items: stretch;\r\n\tjustify-content: flex-start;\r\n\toverflow: scroll;\r\n}\r\n\r\n.field-group\r\n{\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.field-header\r\n{\r\n\tflex-shrink: 0;\r\n}\r\n\r\n.field\r\n{\r\n\tcolor: white;\r\n\tfont-size: 14px;\r\n\tflex-shrink: 0;\r\n\tflex-grow: 0;\r\n\r\n\t> .label\r\n\t{\r\n\t\tflex-grow: 0;\r\n\t\tflex-shrink: 0;\r\n\t\tfont-weight: 600;\r\n\t\topacity: 0.4;\r\n\t\twidth: 20%;\r\n\t\tfont-size: 13px;\r\n\t}\r\n\r\n\t> .control\r\n\t{\r\n\t\tflex-shrink: 0;\r\n\t\tflex-grow: 1;\r\n\t\tflex-direction: column;\r\n\t}\r\n}\r\n\r\n.is-vertical > .field, .field.is-vertical\r\n{\r\n\tflex-direction: column;\r\n\r\n\t> .label\r\n\t{\r\n\t\twidth: auto;\r\n\t\theight: auto;\r\n\t}\r\n}"
        },
        {
            "Ident": "rue.house",
            "Path": "ui/controls/color/colorpickercontrol.cs.scss",
            "FileName": "colorpickercontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "ColorPickerControl\r\n{\r\n\tflex-direction: column;\r\n\tflex-shrink: 0;\r\n\tgap: 0.5rem;\r\n\tmargin: 1rem;\r\n}"
        },
        {
            "Ident": "rue.house",
            "Path": "ui/controls/enumcontrol.cs.scss",
            "FileName": "enumcontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "EnumControl\r\n{\r\n\tgap: 2px;\r\n\tflex-grow: 1;\r\n}\r\n\r\nEnumControl DropDown,\r\nEnumControl ButtonGroup\r\n{\r\n\tborder-radius: 8px;\r\n\tbackground-color: #000a;\r\n\tflex-grow: 1;\r\n}\r\n\r\nEnumControl DropDown\r\n{\r\n\tflex-grow: 1;\r\n\tmin-height: 32px;\r\n}\r\n\r\nEnumControl ButtonGroup\r\n{\r\n\tborder-radius: 12px;\r\n\toverflow: hidden;\r\n\tmin-height: 32px;\r\n\r\n\tButton\r\n\t{\r\n\t\tflex-grow: 1;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\tgap: 4px;\r\n\t\tcolor: #aaa;\r\n\t\tfont-size: 1rem;\r\n\t\tcursor: pointer;\r\n\r\n\t\t.icon\r\n\t\t{\r\n\t\t\tcolor: #08f;\r\n\t\t}\r\n\r\n\t\t&:hover\r\n\t\t{\r\n\t\t\tcolor: #ddd;\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #3af;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&:active\r\n\t\t{\r\n\t\t\tbackground-color: #04a;\r\n\t\t\tcolor: white;\r\n\t\t\ttransform: translateX( 1px ) translateY( 1px );\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #fff;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t&.active\r\n\t\t{\r\n\t\t\tbackground-color: #08f;\r\n\t\t\tcolor: white;\r\n\t\t\tpointer-events: none;\r\n\r\n\t\t\t.icon\r\n\t\t\t{\r\n\t\t\t\tcolor: #fff;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}"
        },
        {
            "Ident": "rue.house",
            "Path": "ui/controls/color/coloralphacontrol.cs.scss",
            "FileName": "coloralphacontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "ColorAlphaControl\r\n{\r\n\tgap: 0.5rem;\r\n\tflex-grow: 1;\r\n\tpointer-events: all;\r\n\tbackground: linear-gradient( to right, black, white );\r\n\tborder-radius: 4px;\r\n\tpadding: 2px;\r\n\theight: 12px;\r\n\tposition: relative;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\ttop: -5px;\r\n\t\tbottom: -5px;\r\n\t\taspect-ratio: 1;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% );\r\n\t\tpointer-events: none;\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "rue.house",
            "Path": "ui/controls/color/colorsaturationvaluecontrol.cs.scss",
            "FileName": "colorsaturationvaluecontrol.cs.scss",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "ColorSaturationValueControl\r\n{\r\n\twidth: 240px;\r\n\theight: 240px;\r\n\tbackground-color: red;\r\n\tposition: relative;\r\n\tborder-radius: 4px;\r\n\tcursor: pointer;\r\n\tborder: 1px solid #333;\r\n\r\n\t&:hover\r\n\t{\r\n\t\tborder: 1px solid #08f;\r\n\t}\r\n\r\n\t&:active\r\n\t{\r\n\t\tborder: 1px solid #fff;\r\n\t}\r\n\r\n\t.handle\r\n\t{\r\n\t\twidth: 16px;\r\n\t\theight: 16px;\r\n\t\tborder-radius: 100px;\r\n\t\tborder: 2px solid #444;\r\n\t\tposition: absolute;\r\n\t\tbackground-color: white;\r\n\t\tbox-shadow: 2px 2px 16px #000a;\r\n\t\ttransform: translateX( -50% ) translateY( -50% );\r\n\t\tpointer-events: none;\r\n\t\tz-index: 100;\r\n\t\tz-index: 100;\r\n\t}\r\n\r\n\t.gradient\r\n\t{\r\n\t\tposition: absolute;\r\n\t\twidth: 100%;\r\n\t\theight: 100%;\r\n\t\tborder-radius: 4px;\r\n\t\tbackground: linear-gradient( to right, white, rgba( 255, 255, 255, 0 ) );\r\n\r\n\t\t&:after\r\n\t\t{\r\n\t\t\tcontent: \"\";\r\n\t\t\tposition: absolute;\r\n\t\t\twidth: 100%;\r\n\t\t\theight: 100%;\r\n\t\t\tborder-radius: 4px;\r\n\t\t\tbackground: linear-gradient( to top, black, rgba( 0, 0, 0, 0 ) );\r\n\t\t}\r\n\t}\r\n}\r\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Grid/Grid.cs",
            "FileName": "Grid.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Sandbox;\n\nnamespace GridAStar;\n\npublic partial class Grid : IValid\n{\n\tpublic static Grid Main\n\t{\n\t\tget => Grids.GetValueOrDefault( \"main\" );\n\t\tset\n\t\t{\n\t\t\tif ( Grids.ContainsKey( \"main\" ) )\n\t\t\t\tGrids[\"main\"] = value;\n\t\t\telse\n\t\t\t\tGrids.Add( \"main\", value );\n\t\t}\n\t}\n\n\tpublic static Dictionary<string, Grid> Grids { get; set; } = new();\n\n\t/// <summary>The scene this grid traces against. Set on creation (Scene-System port).</summary>\n\tpublic Scene Scene { get; set; }\n\n\t// --- Generation diagnostics ---\n\tpublic static int DebugCastsHit;\n\tpublic static int DebugAngleRejected;\n\tpublic static int DebugOutOfBounds;\n\n\tpublic GridBuilder Settings { get; internal set; }\n\tpublic string Identifier => Settings.Identifier;\n\tpublic Dictionary<IntVector2, List<Cell>> CellStacks { get; internal set; } = new();\n\tpublic IEnumerable<Cell> AllCells => CellStacks.Values.SelectMany( list => list );\n\tpublic Vector3 Position => Settings.Position;\n\tpublic BBox Bounds => Settings.Bounds;\n\tpublic BBox RotatedBounds => Bounds.GetRotatedBounds( Rotation );\n\tpublic BBox WorldBounds => RotatedBounds.Translate( Position );\n\tpublic Transform Transform => new Transform( WorldBounds.Center, AxisRotation );\n\tpublic Rotation Rotation => Settings.Rotation;\n\tpublic bool AxisAligned => Settings.AxisAligned;\n\tpublic float StandableAngle => Settings.StandableAngle;\n\tpublic float StepSize => Settings.StepSize;\n\tpublic float CellSize => Settings.CellSize;\n\tpublic float HeightClearance => Settings.HeightClearance;\n\tpublic float WidthClearance => Settings.WidthClearance;\n\tpublic bool GridPerfect => Settings.GridPerfect;\n\tpublic bool StaticOnly => Settings.StaticOnly;\n\tpublic float MaxDropHeight => Settings.MaxDropHeight;\n\tpublic List<JumpDefinition> JumpDefinitions => Settings.JumpDefinitions;\n\tpublic int MinNeighbourCount => Settings.MinNeighbourCount;\n\tpublic bool IgnoreConnectionsForJumps => Settings.IgnoreConnectionsForJumps;\n\tpublic bool IgnoreLOSForJumps => Settings.IgnoreLOSForJumps;\n\tpublic bool CylinderShaped => Settings.CylinderShaped;\n\tpublic float Tolerance => GridPerfect ? 0.001f : 0f;\n\tpublic Rotation AxisRotation => AxisAligned ? new Rotation() : Rotation;\n\tpublic int MinimumColumn => WorldBounds.Mins.ToIntVector2( CellSize ).y;\n\tpublic int MaximumColumn => WorldBounds.Maxs.ToIntVector2( CellSize ).y;\n\tpublic int Columns => MaximumColumn - MinimumColumn;\n\tpublic int MinimumRow => WorldBounds.Mins.ToIntVector2( CellSize ).x;\n\tpublic int MaximumRow => WorldBounds.Maxs.ToIntVector2( CellSize ).x;\n\tpublic int Rows => MaximumRow - MinimumRow;\n\tbool IValid.IsValid { get; }\n\n\tpublic Grid()\n\t{\n\t\tSettings = new GridBuilder();\n\t}\n\n\tpublic Grid( GridBuilder settings )\n\t{\n\t\tSettings = settings;\n\t}\n\n\tpublic void Print( string message ) => Print( Identifier, message );\n\tpublic static void Print( string identifier, string message ) => Log.Info( $\"Grid '{identifier}': {message}\" );\n\n\tpublic BBox ToWorld( BBox bounds ) => bounds.GetRotatedBounds( AxisRotation ).Translate( WorldBounds.Center );\n\tpublic BBox ToLocal( BBox bounds ) => bounds.GetRotatedBounds( AxisRotation.Inverse ).Translate( -WorldBounds.Center );\n\n\tpublic IntVector2 PositionToCoordinates( Vector3 position ) => (position - WorldBounds.Mins - CellSize / 2).ToIntVector2( CellSize );\n\n\t/// <summary>Find the nearest cell from a position even if outside the grid (expensive).</summary>\n\tpublic Cell GetNearestCell( Vector3 position, bool onlyBelow = true, bool unoccupiedOnly = false )\n\t{\n\t\tvar validCells = AllCells;\n\n\t\tif ( unoccupiedOnly )\n\t\t\tvalidCells = validCells.Where( x => !x.Occupied );\n\t\tif ( onlyBelow )\n\t\t\tvalidCells = validCells.Where( x => x.Vertices.Min() - Math.Max( HeightClearance, StepSize ) <= position.z );\n\n\t\treturn validCells.OrderBy( x => x.Position.DistanceSquared( position ) )\n\t\t\t.FirstOrDefault();\n\t}\n\n\tpublic Cell GetCellInArea( Vector3 position, float width, bool onlyBelow = true, bool withinStepRange = true )\n\t{\n\t\tvar cellsToCheck = (int)Math.Ceiling( width / CellSize ) * 2;\n\t\tfor ( int y = 0; y <= cellsToCheck; y++ )\n\t\t{\n\t\t\tvar spiralY = MathAStar.SpiralPattern( y );\n\t\t\tfor ( int x = 0; x <= cellsToCheck; x++ )\n\t\t\t{\n\t\t\t\tvar spiralX = MathAStar.SpiralPattern( x );\n\t\t\t\tvar cellFound = GetCell( position + AxisRotation.Forward * spiralX * CellSize + AxisRotation.Right * spiralY * CellSize + Vector3.Up * StepSize, onlyBelow );\n\n\t\t\t\tif ( cellFound == null ) continue;\n\n\t\t\t\tif ( withinStepRange )\n\t\t\t\t\tif ( position.z - cellFound.Position.z <= Math.Max( HeightClearance, StepSize ) ) return cellFound; else continue;\n\n\t\t\t\treturn cellFound;\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tpublic Cell GetCell( Vector3 position, bool onlyBelow = true ) => GetCell( PositionToCoordinates( position ), onlyBelow ? position.z : WorldBounds.Maxs.z );\n\n\tpublic Cell GetCell( IntVector2 coordinates, float height )\n\t{\n\t\tvar cellsAtCoordinates = CellStacks.GetValueOrDefault( coordinates );\n\n\t\tif ( cellsAtCoordinates == null ) return null;\n\n\t\t// Return the cell CLOSEST to the query height among the candidates, not the first match. A column on\n\t\t// a spiral staircase / multi-floor area stacks several cells at the same XY; the original first-match\n\t\t// returned an arbitrary one (often the bottom of the spiral), so an NPC partway up got a path from the\n\t\t// wrong height and couldn't follow it. Candidate window (<= height + clearance) is unchanged.\n\t\tCell best = null;\n\t\tvar bestDist = float.MaxValue;\n\n\t\tforeach ( var cell in cellsAtCoordinates )\n\t\t{\n\t\t\tif ( cell.Vertices.Min() - Math.Max( HeightClearance, StepSize ) >= height )\n\t\t\t\tcontinue;\n\n\t\t\tvar dist = Math.Abs( cell.Position.z - height );\n\t\t\tif ( dist < bestDist )\n\t\t\t{\n\t\t\t\tbestDist = dist;\n\t\t\t\tbest = cell;\n\t\t\t}\n\t\t}\n\n\t\treturn best;\n\t}\n\n\tpublic void AddCell( Cell cell )\n\t{\n\t\tif ( cell == null ) return;\n\t\tvar coordinates = cell.GridPosition;\n\t\tif ( !CellStacks.ContainsKey( coordinates ) )\n\t\t\tCellStacks.Add( coordinates, new List<Cell>() { cell } );\n\t\telse\n\t\t\tif ( !CellStacks[coordinates].Any( x => Math.Abs( x.Position.z - cell.Position.z ) < Math.Max( HeightClearance, StepSize ) ) )\n\t\t\tCellStacks[coordinates].Add( cell );\n\t}\n\n\tpublic Cell GetCellInDirection( Cell startingCell, Vector3 direction, int numOfCellsInDirection = 1 ) => GetCell( startingCell.Position + direction * CellSize * numOfCellsInDirection );\n\n\tpublic Cell GetNeighbourInDirection( Cell cell, Vector3 direction )\n\t{\n\t\tvar horizontalDirection = direction.WithZ( 0 ).Normal;\n\t\tvar localCoordinates = horizontalDirection.ToIntVector2();\n\t\tvar coordinatesToCheck = cell.GridPosition + localCoordinates;\n\n\t\tvar cellsAtCoordinates = CellStacks.GetValueOrDefault( coordinatesToCheck );\n\n\t\tif ( cellsAtCoordinates == null ) return null;\n\n\t\tforeach ( var cellAtCoordinate in cellsAtCoordinates )\n\t\t\tif ( cell.IsNeighbour( cellAtCoordinate ) && cell != cellAtCoordinate )\n\t\t\t\treturn cellAtCoordinate;\n\n\t\treturn null;\n\t}\n\n\t/// <summary>Returns if there's a valid, unoccupied, and direct line of sight from a cell to another</summary>\n\tpublic bool LineOfSight( Cell startingCell, Cell endingCell, Component pathCreator = null, bool debugShow = false )\n\t{\n\t\tvar startingPosition = startingCell.Position;\n\t\tvar endingPosition = endingCell.Position;\n\t\tvar distanceInSteps = (int)Math.Ceiling( startingPosition.Distance( endingPosition ) / CellSize );\n\n\t\tif ( pathCreator == null && startingCell.Occupied ) return false;\n\t\tif ( pathCreator != null && startingCell.Occupied && startingCell.OccupyingEntity != pathCreator ) return false;\n\n\t\tif ( pathCreator == null && endingCell.Occupied ) return false;\n\t\tif ( pathCreator != null && endingCell.Occupied && endingCell.OccupyingEntity != pathCreator ) return false;\n\n\t\tCell lastCell = startingCell;\n\t\tfor ( int i = 0; i <= distanceInSteps; i++ )\n\t\t{\n\t\t\tvar direction = (endingPosition - lastCell.Position).Normal;\n\t\t\tvar cellToCheck = GetNeighbourInDirection( lastCell, direction );\n\n\t\t\tif ( cellToCheck == null ) return false;\n\t\t\tif ( cellToCheck == endingCell ) return true;\n\t\t\tif ( cellToCheck == lastCell ) continue;\n\t\t\tif ( pathCreator == null && cellToCheck.Occupied ) return false;\n\t\t\tif ( pathCreator != null && cellToCheck.Occupied && cellToCheck.OccupyingEntity != pathCreator ) return false;\n\t\t\tif ( !cellToCheck.IsNeighbour( lastCell ) ) return false;\n\n\t\t\tlastCell = cellToCheck;\n\n\t\t\tif ( debugShow )\n\t\t\t\tlastCell.Draw( 2f, false, false, false );\n\t\t}\n\n\t\treturn true;\n\t}\n\n\t/// <summary>Can you roughly walk towards the cell without it being a direct line of sight</summary>\n\tpublic bool IsDirectlyWalkable( Cell startingCell, Cell endingCell, float maxDistanceFromDirectPath = 150f, Component pathCreator = null, bool withConnections = true )\n\t{\n\t\tif ( startingCell == null || endingCell == null ) return false;\n\n\t\tvar currentCell = startingCell;\n\t\tvar directPath = new Line( startingCell.Position.WithZ( 0 ), endingCell.Position.WithZ( 0 ) );\n\t\tList<Cell> cellsChecked = new();\n\n\t\tif ( pathCreator == null && startingCell.Occupied ) return false;\n\t\tif ( pathCreator != null && startingCell.Occupied && startingCell.OccupyingEntity != pathCreator ) return false;\n\n\t\tif ( pathCreator == null && endingCell.Occupied ) return false;\n\t\tif ( pathCreator != null && endingCell.Occupied && endingCell.OccupyingEntity != pathCreator ) return false;\n\n\t\twhile ( currentCell != endingCell && directPath.Distance( currentCell.Position.WithZ( 0 ) ) <= maxDistanceFromDirectPath )\n\t\t{\n\t\t\tvar cellToCheck = withConnections ? currentCell.GetClosestNeighbourAndConnection( endingCell.Position ) : currentCell.GetClosestNeighbour( endingCell.Position );\n\n\t\t\tif ( cellToCheck == null ) return false;\n\t\t\tif ( cellsChecked.Contains( cellToCheck ) ) return false;\n\t\t\tif ( pathCreator == null && cellToCheck.Occupied ) return false;\n\t\t\tif ( pathCreator != null && cellToCheck.Occupied && cellToCheck.OccupyingEntity != pathCreator ) return false;\n\n\t\t\tif ( cellToCheck == endingCell ) return true;\n\n\t\t\tcellsChecked.Add( currentCell );\n\t\t\tcurrentCell = cellToCheck;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tpublic bool IsInsideBounds( Vector3 point ) => Bounds.IsRotatedPointWithinBounds( Position, point, Rotation );\n\tpublic bool IsInsideCylinder( Vector3 point ) => Bounds.IsInsideSquishedRotatedCylinder( Position, point, Rotation );\n\n\tpublic void Initialize()\n\t{\n\t\tif ( Grids.ContainsKey( Identifier ) )\n\t\t{\n\t\t\tif ( Grids[Identifier] != null )\n\t\t\t\tGrids[Identifier].Delete( true );\n\n\t\t\tGrids[Identifier] = this;\n\t\t}\n\t\telse\n\t\t\tGrids.Add( Identifier, this );\n\t}\n\n\tpublic void Delete( bool deleteSave = false )\n\t{\n\t\tif ( Grids.ContainsKey( Identifier ) )\n\t\t{\n\t\t\tGrids[Identifier] = null;\n\t\t\tGrids.Remove( Identifier );\n\t\t}\n\t}\n\n\tpublic List<Cell> GetCellsInBBox( BBox bbox )\n\t{\n\t\tvar cells = new List<Cell>();\n\n\t\tforeach ( var cell in AllCells )\n\t\t\tif ( bbox.Contains( cell.Position ) )\n\t\t\t\tcells.Add( cell );\n\n\t\treturn cells;\n\t}\n\n\tpublic override int GetHashCode() => Settings.GetHashCode();\n\n\t/// <summary>Gives the edge tag to all cells with less than 8 neighbours</summary>\n\tpublic async Task AssignEdgeCells( int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = \"\", string tagToAssign = \"edge\" ) => await assignEdgeCellsInternal( AllCells.ToList(), maxNeighourCount, threadsToUse, clearTags, tagToExclude, tagToAssign );\n\n\tpublic async Task AssignEdgeCells( BBox bounds, int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = \"\", string tagToAssign = \"edge\" ) => await assignEdgeCellsInternal( GetCellsInBBox( bounds ), maxNeighourCount, threadsToUse, clearTags, tagToExclude, tagToAssign );\n\n\tinternal async Task assignEdgeCellsInternal( List<Cell> cells, int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = \"\", string tagToAssign = \"edge\" )\n\t{\n\t\tvar cellsCount = cells.Count();\n\t\tthreadsToUse = Math.Max( 1, threadsToUse );\n\t\tvar cellsEachThread = (int)(cellsCount / threadsToUse);\n\t\tvar lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));\n\t\tList<Task> tasks = new();\n\n\t\tfor ( int i = 0; i < threadsToUse; i++ )\n\t\t{\n\t\t\tvar curentThread = i;\n\n\t\t\ttasks.Add( GameTask.RunInThreadAsync( () =>\n\t\t\t{\n\t\t\t\tvar cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;\n\t\t\t\tvar cellsToCheck = cells.Skip( cellsEachThread * curentThread ).Take( cellsRange );\n\n\t\t\t\tforeach ( var cell in cellsToCheck )\n\t\t\t\t{\n\t\t\t\t\tif ( clearTags )\n\t\t\t\t\t\tcell.Tags.Remove( tagToAssign );\n\n\t\t\t\t\tvar neighbours = cell.GetNeighbours();\n\n\t\t\t\t\tif ( tagToExclude != \"\" )\n\t\t\t\t\t\tneighbours = neighbours.Where( x => !x.Tags.Has( tagToExclude ) );\n\n\t\t\t\t\tif ( neighbours.Count() < maxNeighourCount )\n\t\t\t\t\t\tcell.Tags.Add( tagToAssign );\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tawait GameTask.WhenAll( tasks );\n\t}\n\n\t/// <summary>Adds the droppable connection to cells you can drop from</summary>\n\tpublic async Task AssignDroppableCells( int threadsToUse = 1 ) => await internalAssignDroppableCells( CellsWithTag( \"edge\" ).ToList(), threadsToUse );\n\n\tpublic async Task AssignDroppableCells( BBox bounds, int threadsToUse = 1 ) => await internalAssignDroppableCells( CellsWithTag( bounds, \"edge\" ).ToList(), threadsToUse );\n\n\tinternal async Task internalAssignDroppableCells( List<Cell> cells, int threadsToUse = 1 )\n\t{\n\t\tvar allCells = cells;\n\t\tvar cellsCount = allCells.Count();\n\t\tthreadsToUse = Math.Max( 1, threadsToUse );\n\t\tvar cellsEachThread = (int)(cellsCount / threadsToUse);\n\t\tvar lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));\n\t\tList<Task> tasks = new();\n\n\t\tfor ( int i = 0; i < threadsToUse; i++ )\n\t\t{\n\t\t\tvar curentThread = i;\n\n\t\t\ttasks.Add( GameTask.RunInThreadAsync( () =>\n\t\t\t{\n\t\t\t\tvar cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;\n\t\t\t\tvar cellsToCheck = allCells.Skip( cellsEachThread * curentThread ).Take( cellsRange );\n\n\t\t\t\tforeach ( var cell in cellsToCheck )\n\t\t\t\t{\n\t\t\t\t\tvar droppableCell = cell.GetFirstValidDroppable( maxHeightDistance: MaxDropHeight );\n\t\t\t\t\tif ( droppableCell != null )\n\t\t\t\t\t\tcell.AddConnection( droppableCell, \"drop\" );\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tawait GameTask.WhenAll( tasks );\n\t}\n\n\tpublic IEnumerable<Cell> JumpableCandidates()\n\t{\n\t\tvar droppedCells = CellsWithConnection( \"drop\" ).SelectMany( cell => cell.GetConnections( \"drop\" ).Select( connection => connection.Current ) );\n\t\treturn CellsWithTag( \"edge\" ).Concat( droppedCells );\n\t}\n\n\tpublic async Task AssignJumpableCells( JumpDefinition definition, int threadsToUse = 16 ) => await internalAssignJumpableCells( JumpableCandidates().ToList(), definition, threadsToUse );\n\n\tinternal async Task internalAssignJumpableCells( List<Cell> cells, JumpDefinition definition, int threadsToUse = 16 )\n\t{\n\t\tvar allCells = cells;\n\t\tvar cellsCount = allCells.Count();\n\t\tthreadsToUse = Math.Max( 1, threadsToUse );\n\t\tvar cellsEachThread = (int)(cellsCount / threadsToUse);\n\t\tvar lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));\n\t\tList<Task> tasks = new();\n\n\t\tfor ( int i = 0; i < threadsToUse; i++ )\n\t\t{\n\t\t\tvar curentThread = i;\n\n\t\t\ttasks.Add( GameTask.RunInThreadAsync( () =>\n\t\t\t{\n\t\t\t\tvar totalFraction = 1f;\n\t\t\t\tvar cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;\n\t\t\t\tvar cellsToCheck = allCells.Skip( cellsEachThread * curentThread ).Take( cellsRange );\n\n\t\t\t\tforeach ( var cell in cellsToCheck )\n\t\t\t\t{\n\t\t\t\t\tif ( totalFraction >= 1f )\n\t\t\t\t\t{\n\t\t\t\t\t\tList<Cell> connectedCells = new();\n\t\t\t\t\t\tList<AStarNode> jumpConnections = new();\n\n\t\t\t\t\t\tforeach ( var jumpableCell in cell.GetValidJumpables( definition, MaxDropHeight, IgnoreConnectionsForJumps, IgnoreLOSForJumps ) )\n\t\t\t\t\t\t\tif ( jumpableCell != null )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tjumpConnections.Add( cell.AddConnection( jumpableCell, definition.Name ) );\n\t\t\t\t\t\t\t\tconnectedCells.Add( jumpableCell );\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\tforeach ( var jumpableConnection in connectedCells )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar direction = (cell.Position - jumpableConnection.Position).WithZ( 0 ).Normal;\n\t\t\t\t\t\t\tvar jumpbackCell = jumpableConnection.GetValidJumpable( definition, direction, MaxDropHeight, IgnoreConnectionsForJumps, IgnoreLOSForJumps );\n\n\t\t\t\t\t\t\tif ( jumpbackCell != null )\n\t\t\t\t\t\t\t\tif ( !IsDirectlyWalkable( jumpbackCell, cell ) )\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tvar duplicate = false;\n\t\t\t\t\t\t\t\t\tforeach ( var connection in jumpConnections )\n\t\t\t\t\t\t\t\t\t\tif ( connection.Parent.Current == jumpbackCell && connection.MovementTag == definition.Name )\n\t\t\t\t\t\t\t\t\t\t\tduplicate = true;\n\t\t\t\t\t\t\t\t\tif ( !duplicate )\n\t\t\t\t\t\t\t\t\t\tjumpConnections.Add( jumpableConnection.AddConnection( jumpbackCell, definition.Name ) );\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tforeach ( var connection in jumpConnections )\n\t\t\t\t\t\t\tif ( LineOfSight( connection.Parent.Current, connection.Current ) )\n\t\t\t\t\t\t\t\tconnection.Parent.Current.RemoveConnection( connection );\n\n\t\t\t\t\t\ttotalFraction = 0f;\n\t\t\t\t\t}\n\n\t\t\t\t\ttotalFraction += definition.GenerateFraction;\n\t\t\t\t}\n\t\t\t} ) );\n\t\t}\n\n\t\tawait GameTask.WhenAll( tasks );\n\t}\n\n\tpublic Vector3 TraceParabola( Vector3 startingPosition, Vector3 horizontalVelocity, float verticalSpeed, float gravity, float maxDropHeight, int subSteps = 2 )\n\t{\n\t\tvar horizontalDirection = horizontalVelocity.WithZ( 0 ).Normal;\n\t\tvar horizontalSpeed = horizontalVelocity.WithZ( 0 ).Length;\n\t\tvar maxHeight = startingPosition.z + MathAStar.ParabolaMaxHeight( verticalSpeed, gravity );\n\t\tvar minHeight = maxHeight - maxDropHeight;\n\t\tvar currentDistance = 1;\n\t\tvar lastPositionChecked = startingPosition;\n\n\t\twhile ( lastPositionChecked.z >= minHeight )\n\t\t{\n\t\t\tvar horizontalOffset = CellSize * currentDistance / subSteps;\n\t\t\tvar verticalOffset = MathAStar.ParabolaHeight( horizontalOffset, horizontalSpeed, verticalSpeed, gravity );\n\t\t\tvar nextPositionToCheck = startingPosition + horizontalDirection * horizontalOffset + Vector3.Up * verticalOffset;\n\n\t\t\tvar clearanceBBox = new BBox( new Vector3( -WidthClearance / 2f, -WidthClearance / 2f, StepSize ), new Vector3( WidthClearance / 2f, WidthClearance / 2f, HeightClearance ) );\n\t\t\tvar jumpTrace = Scene.Trace.Box( clearanceBBox, lastPositionChecked, nextPositionToCheck )\n\t\t\t\t.WithGridSettings( Settings )\n\t\t\t\t.Run();\n\n\t\t\tif ( jumpTrace.Hit )\n\t\t\t\treturn jumpTrace.EndPosition;\n\n\t\t\tlastPositionChecked = nextPositionToCheck;\n\t\t\tcurrentDistance++;\n\t\t}\n\n\t\treturn lastPositionChecked;\n\t}\n\n\tpublic void RemoveCells( BBox bounds, bool printInfo = false )\n\t{\n\t\tvar cellsToRemove = GetCellsInBBox( bounds );\n\t\tvar count = cellsToRemove.Count();\n\n\t\tforeach ( var cell in cellsToRemove )\n\t\t\tcell.Delete();\n\n\t\tif ( printInfo )\n\t\t\tPrint( $\"Removed {count} cells\" );\n\t}\n\n\tpublic async Task GenerateCells( BBox bounds, int threadedChunkSides = 1, bool printInfo = true )\n\t{\n\t\tList<Task<List<Cell>>> tasks = new();\n\t\tvar totalMins = bounds.Mins;\n\t\tvar totalMaxs = bounds.Maxs;\n\t\tvar totalSize = bounds.Size;\n\n\t\tthreadedChunkSides = Math.Max( 1, threadedChunkSides );\n\n\t\tfor ( int x = 1; x <= threadedChunkSides; x++ )\n\t\t{\n\t\t\tfor ( int y = 1; y <= threadedChunkSides; y++ )\n\t\t\t{\n\t\t\t\tvar xOffset = totalSize.x / threadedChunkSides * x - totalSize.x / threadedChunkSides / 2;\n\t\t\t\tvar yOffset = totalSize.y / threadedChunkSides * y - totalSize.y / threadedChunkSides / 2;\n\t\t\t\tvar offset = new Vector3( xOffset, yOffset );\n\t\t\t\tvar chunkSize = totalSize / threadedChunkSides;\n\t\t\t\tvar chunkMins = totalMins + offset - chunkSize / 2;\n\t\t\t\tvar chunkMaxs = totalMins + offset + chunkSize / 2;\n\t\t\t\tvar dividedBounds = new BBox( chunkMins.WithZ( totalMins.z ), chunkMaxs.WithZ( totalMaxs.z ) );\n\n\t\t\t\ttasks.Add( GameTask.RunInThreadAsync( () => createCells( dividedBounds, printInfo ) ) );\n\t\t\t}\n\t\t}\n\n\t\tawait GameTask.WhenAll( tasks );\n\n\t\tforeach ( var task in tasks )\n\t\t\tforeach ( var cell in task.Result )\n\t\t\t\tAddCell( cell );\n\t}\n\n\t/// <summary>Create cells in that local bbox (Doesn't add them)</summary>\n\tprivate List<Cell> createCells( BBox bounds, bool printInfo = true )\n\t{\n\t\tvar generatedCells = new List<Cell>();\n\n\t\tvar minimumGrid = bounds.Mins.ToIntVector2( CellSize );\n\t\tvar maximumGrid = bounds.Maxs.ToIntVector2( CellSize );\n\t\tvar startingColumn = minimumGrid.y - MinimumColumn;\n\t\tvar totalColumns = maximumGrid.y - minimumGrid.y;\n\t\tvar endingColumn = startingColumn + totalColumns;\n\t\tvar startingRow = minimumGrid.x - MinimumRow;\n\t\tvar totalRows = maximumGrid.x - minimumGrid.x;\n\t\tvar endingRow = startingRow + totalRows;\n\n\t\tfor ( int column = startingColumn; column < endingColumn; column++ )\n\t\t{\n\t\t\tfor ( int row = startingRow; row < endingRow; row++ )\n\t\t\t{\n\t\t\t\tvar startPosition = WorldBounds.Mins.WithZ( WorldBounds.Maxs.z ) + new Vector3( row * CellSize + CellSize / 2f, column * CellSize + CellSize / 2f, Tolerance * 2f ) * AxisRotation;\n\t\t\t\tvar endPosition = WorldBounds.Mins + new Vector3( row * CellSize + CellSize / 2f, column * CellSize + CellSize / 2f, -Tolerance ) * AxisRotation;\n\t\t\t\tvar checkBBox = new BBox( new Vector3( -CellSize / 2f + Tolerance, -CellSize / 2f + Tolerance, 0f ), new Vector3( CellSize / 2f - Tolerance, CellSize / 2f - Tolerance, 0.001f ) );\n\t\t\t\tvar positionTrace = Scene.Trace.Box( checkBBox, startPosition, endPosition )\n\t\t\t\t\t.WithGridSettings( Settings );\n\n\t\t\t\tvar positionResult = positionTrace.Run();\n\n\t\t\t\twhile ( positionResult.Hit && startPosition.z >= endPosition.z )\n\t\t\t\t{\n\t\t\t\t\tDebugCastsHit++;\n\t\t\t\t\tif ( IsInsideBounds( positionResult.HitPosition ) )\n\t\t\t\t\t{\n\t\t\t\t\t\tif ( !CylinderShaped || IsInsideCylinder( positionResult.HitPosition ) )\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tvar angle = Vector3.GetAngle( Vector3.Up, positionResult.Normal );\n\t\t\t\t\t\t\tif ( angle <= StandableAngle )\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tvar newCell = Cell.TryCreate( this, positionResult.HitPosition );\n\n\t\t\t\t\t\t\t\tif ( newCell != null )\n\t\t\t\t\t\t\t\t\tgeneratedCells.Add( newCell );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tDebugAngleRejected++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\telse\n\t\t\t\t\t{\n\t\t\t\t\t\tDebugOutOfBounds++;\n\t\t\t\t\t}\n\n\t\t\t\t\tstartPosition = positionResult.HitPosition + Vector3.Down * HeightClearance;\n\n\t\t\t\t\t// Scene-System port of Sandbox.Trace.TestPoint: a zero-length sphere trace reports\n\t\t\t\t\t// StartedSolid when the point is inside geometry. Step down until we're clear.\n\t\t\t\t\twhile ( Scene.Trace.Sphere( CellSize / 2f - Tolerance, startPosition, startPosition ).Run().StartedSolid )\n\t\t\t\t\t\tstartPosition += Vector3.Down * HeightClearance;\n\n\t\t\t\t\tpositionTrace = Scene.Trace.Box( checkBBox, startPosition, endPosition )\n\t\t\t\t\t\t.WithGridSettings( Settings );\n\n\t\t\t\t\tpositionResult = positionTrace.Run();\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn generatedCells;\n\t}\n\n\tpublic IEnumerable<Cell> CellsWithTag( string tag ) => AllCells.Where( cell => cell.Tags.Has( tag ) );\n\tpublic IEnumerable<Cell> CellsWithTags( params string[] tags ) => AllCells.Where( cell => cell.Tags.Has( tags ) );\n\tpublic IEnumerable<Cell> CellsWithTags( List<string> tags ) => AllCells.Where( cell => cell.Tags.Has( tags ) );\n\tpublic IEnumerable<Cell> CellsWithTag( BBox bounds, string tag ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tag ) );\n\tpublic IEnumerable<Cell> CellsWithTags( BBox bounds, params string[] tags ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tags ) );\n\tpublic IEnumerable<Cell> CellsWithTags( BBox bounds, List<string> tags ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tags ) );\n\tpublic IEnumerable<Cell> CellsWithConnection( string movementTag ) => AllCells.Where( cell => cell.GetConnections( movementTag ).Count() > 0 );\n\tpublic IEnumerable<Cell> CellsWithConnection( BBox bounds, string movementTag ) => GetCellsInBBox( bounds ).Where( cell => cell.GetConnections( movementTag ).Count() > 0 );\n\n\tpublic void CheckOccupancy( string tag )\n\t{\n\t\tforeach ( var cell in AllCells )\n\t\t\tcell.Occupied = cell.TestForOccupancy( tag );\n\t}\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Grid/GridSettings.cs",
            "FileName": "GridSettings.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "namespace GridAStar;\n\n// Set STEP_SIZE or WIDTH_CLEARANCE to 0 to disable them (faster grid generation)\npublic static partial class GridSettings\n{\n\tpublic const float DEFAULT_STANDABLE_ANGLE = 40f;   // How steep the terrain can be on a cell before it gets discarded\n\tpublic const float DEFAULT_STEP_SIZE = 12f;         // How big steps can be on a cell before it gets discarded\n\tpublic const float DEFAULT_CELL_SIZE = 16f;         // How large each cell will be in hammer units\n\tpublic const float DEFAULT_HEIGHT_CLEARANCE = 72f;  // How much vertical space there should be\n\tpublic const float DEFAULT_WIDTH_CLEARANCE = 24f;   // How much horizontal space there should be\n\tpublic const float DEFAULT_DROP_HEIGHT = 400f;      // How high you can drop down from\n\tpublic const bool DEFAULT_GRID_PERFECT = false;     // For grid-perfect terrain, if true it will not be checking for steps, so use ramps instead\n\tpublic const bool DEFAULT_STATIC_ONLY = true;       // Will it only hit world and static or also dynamic\n}\n"
        },
        {
            "Ident": "rue.house",
            "Path": "Grid/TraceExtensions.cs",
            "FileName": "TraceExtensions.cs",
            "PackageType": "game",
            "CodeKind": "Game",
            "AssetVersionId": 301999,
            "Code": "using Sandbox;\n\nnamespace GridAStar;\n\npublic static partial class TraceExtensions\n{\n\t/// <summary>\n\t/// Apply a grid's generation filters to a scene trace. Scene-System port: legacy <c>StaticOnly()</c>\n\t/// becomes <c>IgnoreDynamic()</c>. Tag filters are only applied when non-empty (an empty\n\t/// <c>WithAllTags</c> would otherwise filter against nothing).\n\t/// </summary>\n\tpublic static SceneTrace WithGridSettings( this SceneTrace self, GridBuilder settings )\n\t{\n\t\tif ( settings.StaticOnly )\n\t\t\tself = self.IgnoreDynamic();\n\n\t\t// ANY of the include tags (not all) - the floor may be tagged \"world\" while props are \"solid\", and\n\t\t// requiring both would match nothing. Legacy used WithAllTags but its maps used a single floor tag.\n\t\tif ( settings.TagsToInclude.Count > 0 )\n\t\t\tself = self.WithAnyTags( settings.TagsToInclude.ToArray() );\n\n\t\tif ( settings.TagsToExclude.Count > 0 )\n\t\t\tself = self.WithoutTags( settings.TagsToExclude.ToArray() );\n\n\t\treturn self;\n\t}\n}\n"
        }
    ]
}