Entities/GrindPathEntity.cs
using System;

namespace Skateboard.Entities;

[EditorHandle( "moving" )]
[Title( "Grind Path" )]
[Category( "Gameplay" )]
[Icon( "moving" )]
public sealed class GrindPathEntity : Component
{
	[Property] public bool Looped { get; set; }
	[Property] public List<GameObject> PathNodes { get; set; } = new();
	[Property, TextArea, WideMode, ReadOnly] public string PathNodesJson { get; set; }

	private sealed class PathNodeJson
	{
		public Vector3 Position { get; set; }
		public Vector3 TangentIn { get; set; }
		public Vector3 TangentOut { get; set; }
		public string hammerUniqueId { get; set; }
		public string classname { get; set; }
	}

	protected override void OnValidate()
	{
		base.OnValidate();
		RefreshPathNodes();
	}

	protected override void OnStart()
	{
		base.OnStart();
		if ( PathNodes.Count == 0 && !string.IsNullOrWhiteSpace( PathNodesJson ) )
			BuildPathNodesFromJson( PathNodesJson );
	}

	public void SetPathNodesJson( string json )
	{
		PathNodesJson = json;
		if ( PathNodes.Count == 0 )
			BuildPathNodesFromJson( json );
	}

	public void RefreshPathNodes()
	{
		if ( PathNodes is null )
			PathNodes = new List<GameObject>();

		if ( PathNodes.Count > 0 )
			return;

		foreach ( var child in GameObject.Children )
		{
			if ( !child.IsValid() )
				continue;

			if ( child.Components.Get<GrindPathNodeEntity>() is not null )
				PathNodes.Add( child );
		}
	}

	private void BuildPathNodesFromJson( string json )
	{
		if ( string.IsNullOrWhiteSpace( json ) )
			return;

		List<PathNodeJson> nodes;
		try
		{
			nodes = System.Text.Json.JsonSerializer.Deserialize<List<PathNodeJson>>(
				json,
				new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }
			);
		}
		catch ( Exception ex )
		{
			Log.Warning( $"GrindPathEntity failed to parse pathNodesJSON: {ex.Message}" );
			return;
		}

		if ( nodes is null || nodes.Count == 0 )
			return;

		PathNodes.Clear();

		for ( int i = 0; i < nodes.Count; i++ )
		{
			var node = nodes[i];
			var nodeObj = new GameObject( GameObject, true, $"GrindPathNode_{i}" )
			{
				LocalPosition = node.Position
			};

			nodeObj.Components.GetOrCreate<GrindPathNodeEntity>();
			PathNodes.Add( nodeObj );
		}
	}

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

		if ( PathNodes is null || PathNodes.Count < 2 )
			return;

		for ( int i = 0; i < PathNodes.Count - 1; i++ )
		{
			var a = PathNodes[i];
			var b = PathNodes[i + 1];
			if ( !a.IsValid() || !b.IsValid() )
				continue;

			Gizmo.Draw.Color = Color.Green.Darken( 0.5f );
			Gizmo.Draw.LineThickness = 4;
			Gizmo.Draw.Line( a.LocalPosition, b.LocalPosition );
		}

		if ( Looped )
		{
			var start = PathNodes[0];
			var end = PathNodes[^1];
			if ( start.IsValid() && end.IsValid() )
			{
				Gizmo.Draw.Color = Color.Green.Darken( 0.5f );
				Gizmo.Draw.Line( end.LocalPosition, start.LocalPosition );
			}
		}
	}
}