Park/Paths/Path.cs
using System;
using HC3.Persistence;
using HC3.Terrain;
using System.Collections.Immutable;
namespace HC3;
/// <summary>
/// A path connection interface.
/// </summary>
public interface IPathConnector
{
int GetHeightOffset( TileEdge edge ) { return 0; }
/// <summary>
/// Can this path connect at a specific edge?
/// </summary>
/// <param name="gridPos"></param>
/// <param name="edge"></param>
/// <returns></returns>
bool CanConnectTile( Vector3Int gridPos, TileEdge edge );
}
public class Path : Component, Component.ExecuteInEditor, IGridObjectEvent, IPathConnector
{
public const int PATH_HEIGHT = 2;
public const int PATH_COST = 10;
public sealed record SaveData( Vector3Int Position, PathType Type, PathMask Mask, Vector2Int StairDirection );
[ConVar( "hc3.debug.paths" )]
public static bool DebugDraw { get; set; }
public SaveData GetSaveData()
{
return new( TilePosition, PathType, Mask, StairDirection );
}
[RequireComponent]
public GridObject GridObject { get; private set; }
public bool GhostPreview { get; set; }
[Property, Hide] public Vector2Int StairDirection { get; set; }
[Property, Hide] public PathMask Mask { get; private set; }
[Property, Hide] public GameObject Tile { get; private set; }
[Property] public PathType PathType { get; set; }
public bool IsElevated { get; set; }
public bool IsStairs => StairDirection != 0;
public virtual int Hash => HashCode.Combine( PathType, Mask );
private int prevHash;
public Vector3Int TilePosition
{
get
{
var pos2d = GridManager.WorldToGridPosition( WorldPosition );
return new Vector3Int( pos2d.x, pos2d.y, (int)MathF.Round( WorldPosition.z / GridManager.HeightStep ) );
}
}
public int RegionId => GridObject.RegionId;
protected override void OnUpdate()
{
if ( !DebugDraw ) return;
using ( Gizmo.Scope( $"path_{GameObject.Id}" ) )
{
var pos = WorldPosition;
var center = pos + Vector3.Up * 8;
foreach ( var edge in GridManager.AllEdges )
{
var rawMask = edge.ToPathMask();
var isWalkable = IsWalkable( edge );
var cell = GridManager.Instance?.GetCell( new Vector2Int( TilePosition.x, TilePosition.y ) );
var decorationBlocking = false;
if ( cell != null )
{
var decorations = cell.GetComponents<Decoration>( TilePosition.z );
foreach ( var decoration in decorations )
{
var mask = edge.ToPathMask();
var yaw = (int)MathF.Round( decoration.WorldRotation.Yaw() / 90f );
while ( yaw < 0 ) yaw += 4;
yaw %= 4;
// Rotate mask to match decoration
for ( int i = 0; i < yaw; i++ )
mask = mask.Rotate90DegreesClockwise();
if ( (decoration.BlockedDirections & mask) != 0 )
{
decorationBlocking = true;
break;
}
}
}
var dir = edge.GetDirection();
var end = center + new Vector3( dir.x, dir.y, 0 ) * 32;
var color = isWalkable ? Color.Green
: decorationBlocking ? Color.Red
: Color.Yellow;
Gizmo.Draw.Color = color.WithAlpha( 0.5f );
Gizmo.Draw.Line( center, end );
var arrowDir = (end - center).Normal;
var arrowEnd = end - arrowDir * 8;
var right = Vector3.Cross( Vector3.Up, arrowDir ).Normal * 4;
Gizmo.Draw.Line( end, arrowEnd + right );
Gizmo.Draw.Line( end, arrowEnd - right );
if ( !isWalkable )
{
var textPos = Vector3.Lerp( center, end, 0.5f ) + Vector3.Up * 4;
Gizmo.Draw.Color = color;
var info = decorationBlocking ? "Blocked by\nDecoration" : "Blocked";
Gizmo.Draw.Text( info, new Transform( textPos ) );
}
}
}
}
protected virtual void UpdateConnections()
{
}
void IGridObjectEvent.NeighborsChanged( GridCell cell )
{
UpdateConnections();
UpdateTile();
}
protected override void OnEnabled()
{
// hmmmm
GridObject = GetOrAddComponent<GridObject>();
UpdateConnections();
UpdateTile();
}
public bool IsWalkable( TileEdge edge, GridCell.ConnectionFlags flags = GridCell.ConnectionFlags.Default )
{
var pos = TilePosition + edge.GetDirection();
var pos2d = new Vector2Int( pos.x, pos.y );
if ( !GridManager.Instance?.Terrain.Bounds.IsInside( pos2d ) ?? false )
return true;
// Check cached blocked directions in source cell
var cell = GridManager.Instance?.GetCell( new Vector2Int( TilePosition.x, TilePosition.y ) );
if ( cell != null )
{
var mask = edge.ToPathMask();
if ( (cell.GetBlockedDirections( TilePosition.z ) & mask) != 0 )
return false;
}
// Check cached blocked directions in target cell (opposite direction)
var targetCell = GridManager.Instance?.GetCell( pos2d );
if ( targetCell != null )
{
var oppositeMask = edge.GetOpposite().ToPathMask();
if ( (targetCell.GetBlockedDirections( pos.z ) & oppositeMask) != 0 )
return false;
}
if ( !CanConnectTile( TilePosition, edge ) )
return false;
return targetCell?.GetConnection( pos, edge.GetOpposite(), flags ) is not null;
}
public void AlignToTerrain()
{
var terrain = GridManager.Instance?.Terrain;
Vector2Int pos2d = new Vector2Int( TilePosition );
if ( terrain?[pos2d] is { } tile && PathBuilder.IsPathableSlope( tile.Slope ) )
{
StairDirection = tile.Slope.GetGradients();
}
UpdateTile();
}
public void SetStair( TileEdge edge, PathElevation elevation )
{
if ( elevation is PathElevation.Flat )
{
StairDirection = Vector2Int.Zero;
}
else if ( elevation is PathElevation.StairUp )
{
StairDirection = edge.GetDirection();
}
else if ( elevation is PathElevation.StairDown )
{
StairDirection = edge.GetOpposite().GetDirection();
var pos = WorldPosition;
pos.z -= GridManager.HeightStep;
WorldPosition = pos;
}
UpdateTile();
}
private void UpdateMask()
{
var terrain = GridManager.Instance?.Terrain;
var pos = TilePosition;
var pos2d = new Vector2Int( pos.x, pos.y );
if ( StairDirection.Length != 0 )
{
Mask = StairDirection.x != 0
? PathMask.Left | PathMask.Right
: PathMask.Up | PathMask.Down;
}
else
{
Mask = 0;
if ( IsWalkable( TileEdge.Left ) )
Mask |= PathMask.Left;
if ( IsWalkable( TileEdge.Right ) )
Mask |= PathMask.Right;
if ( IsWalkable( TileEdge.Up ) )
Mask |= PathMask.Up;
if ( IsWalkable( TileEdge.Down ) )
Mask |= PathMask.Down;
}
IsElevated = StairDirection.Length != 0 || TilePosition.z > terrain[pos2d].MinHeight;
}
protected void UpdateTile()
{
var tilemap = PathBuilder.Instance.PathTileMap;
if ( PathType == PathType.Queue )
tilemap = PathBuilder.Instance.QueueTileMap;
UpdateMask();
if ( !GhostPreview && prevHash == Hash )
return;
prevHash = Hash;
Tile?.DestroyImmediate();
var (tilePrefab, direction) = tilemap.GetTile( Mask, StairDirection, IsElevated );
if ( tilePrefab is null ) return;
Tile = tilePrefab.Clone( new CloneConfig { Parent = GameObject, StartEnabled = true, Name = "tile" } );
Tile.LocalRotation = Rotation.LookAt( direction );
Tile.Flags |= GameObjectFlags.NotSaved;
if ( GhostPreview )
{
var renderer = Tile.GetComponentInChildren<ModelRenderer>();
if ( renderer.IsValid() )
{
renderer.SceneObject.Attributes.Set( "Ghost", PathBuilder.Instance.CanPlace( TilePosition ) ? 1 : 2 );
renderer.SceneObject.Batchable = false;
}
}
OnTileUpdated();
}
protected virtual void OnTileUpdated() { }
public virtual bool CanConnectTile( Vector3Int gridPos, TileEdge edge )
{
if ( StairDirection == 0 )
{
// Easy case for non-stairs
return gridPos == TilePosition;
}
float dot = Vector2.Dot( edge.GetDirection(), StairDirection );
if ( dot == 0 ) return false; // Can't connect to the side of the stair
return gridPos == TilePosition + Vector3Int.Up * GetHeightOffset( edge );
}
public int GetHeightOffset( TileEdge edge )
{
float dot = Vector2.Dot( edge.GetDirection(), StairDirection );
return dot > 0 ? 1 : 0;
}
public float GetHeightAt( Vector3 worldPos )
{
Vector2 delta2D = Transform.World.PointToLocal( worldPos );
delta2D += StairDirection * (GridManager.GridSize / 2.0f);
float frac = Vector2.Dot( delta2D / GridManager.GridSize, StairDirection );
return WorldPosition.z + (frac * GridManager.HeightStep);
}
}
file sealed class PathSaveData : ISaveDataProperty<ImmutableArray<Path.SaveData>>
{
string ISaveDataProperty.PropertyName => "Paths";
ImmutableArray<Path.SaveData> ISaveDataProperty<ImmutableArray<Path.SaveData>>.WriteValue( Scene scene ) =>
scene.GetAllComponents<Path>()
.Select( x => x.GetSaveData() )
.ToImmutableArray();
void ISaveDataProperty<ImmutableArray<Path.SaveData>>.ReadValue( Scene scene, ImmutableArray<Path.SaveData> model )
{
var pathBuilder = PathBuilder.Instance;
pathBuilder.Clear();
Dictionary<Path, Path.SaveData> paths = new();
foreach ( var data in model )
{
var path = pathBuilder.AddPath( data.Position, data.Type );
if ( path is null ) continue;
path.StairDirection = data.StairDirection;
paths.Add( path, data );
}
foreach ( var queue in paths.Keys.OfType<Queue>() )
{
queue.OnPostLoad( paths[queue] );
}
}
}