Pawn/SkatePawn.cs
using Skateboard.Tricks;
using Skateboard.Utils;
using System;

namespace Skateboard.Player;

public sealed class SkatePawn : Component
{
	public static SkatePawn Local => Game.ActiveScene.GetAll<SkatePawn>().FirstOrDefault( x => x.Network.IsOwner );
	public enum BailType { Normal, Landing, Bail }

	[Property] public SkinnedModelRenderer BodyRenderer { get; set; }
	[Property] public SkinnedModelRenderer BoardRenderer { get; set; }
	[Property] public GameObject Head { get; set; }
	[Property] public string BodyModel { get; set; } = "models/skateanimations.vmdl";
	[Property] public string BoardModel { get; set; } = "models/skateboard_animated.vmdl";

	[Property] public TrickScoreHolder TrickScores { get; set; }
	[Property] public SkateController Controller { get; set; }
	[Property] public SkateAnimator Animator { get; set; }
	[Property] public SkateInput Input { get; set; }

	[Property] public SkateAudioHelper SkateAudio { get; set; }
	[Property] public GrindAudioHelper GrindAudio { get; set; }

	[Sync] public float TurnRight { get; set; }
	[Sync] public bool Crouch { get; set; }
	[Sync] public bool OnVert { get; set; }
	[Sync] public Vector3 VertNormal { get; set; }
	[Sync] public Vector3 Velocity { get; set; }
	[Sync] public Rotation RealRotation { get; set; }
	[Sync] public Guid OwningConnectionId { get; set; }

	[Sync] public float BailTime { get; set; } = 1.5f;
	[Sync] public bool Bailed { get; private set; }

	[Sync] public Vector3 EyeLocalPosition { get; set; }
	[Sync] public Rotation EyeLocalRotation { get; set; }

	private float _timeBailed;

	public GameObject Ragdoll { get; private set; }
	public GameObject BoardRagdoll { get; private set; }

	public Vector3 EyePosition
	{
		get => WorldTransform.PointToWorld( EyeLocalPosition );
		set => EyeLocalPosition = WorldTransform.PointToLocal( value );
	}

	public Rotation EyeRotation
	{
		get => WorldTransform.RotationToWorld( EyeLocalRotation );
		set => EyeLocalRotation = WorldTransform.RotationToLocal( value );
	}

	public bool Grinding => Controller?.OnGrind ?? false;

	protected override void OnStart()
	{
		EnsureRenderers();

		Input ??= Components.GetOrCreate<SkateInput>();
		Controller ??= Components.GetOrCreate<SkateController>();
		Animator ??= Components.GetOrCreate<SkateAnimator>();
		TrickScores ??= Components.GetOrCreate<TrickScoreHolder>();

		SkateAudio ??= Components.GetOrCreate<SkateAudioHelper>();
		GrindAudio ??= Components.GetOrCreate<GrindAudioHelper>();
		Components.GetOrCreate<Skateboard.ClothingModelSuffixStripper>();

		Controller.Pawn = this;
		Controller.Input = Input;
		Controller.TrickScores = TrickScores;
		Controller.Animator = Animator;

		Input.Pawn = this;

		Animator.Pawn = this;
		Animator.Target = BodyRenderer;
		Animator.BoardRenderer = BoardRenderer;

		SkateAudio.Pawn = this;
		GrindAudio.Pawn = this;

		TrickScores.OnComboFinish += OnComboFinish;
		TrickScores.OnComboFailed += OnComboFail;

		TurnRight = 0f;
		Crouch = false;
		RealRotation = WorldRotation;

		Velocity = 0f;
		Bailed = false;
		_timeBailed = 0f;

		Tags.Add( "player" );
	}

	protected override void OnFixedUpdate()
	{
		if ( !IsLocallyControlled() )
			return;

		BodyRenderer.Model = Model.Load( BodyModel );

		if ( Bailed )
		{
			_timeBailed += Time.Delta;
			if ( _timeBailed >= BailTime )
				RespawnAfterBail();
			return;
		}
	}

	private void EnsureRenderers()
	{
		BodyRenderer ??= Components.GetOrCreate<SkinnedModelRenderer>();
		BodyRenderer.Model ??= Model.Load( BodyModel );

		if ( BoardRenderer.IsValid() )
		{
			BoardRenderer.Model ??= Model.Load( BoardModel );
			return;
		}

		var boardObject = new GameObject( GameObject, true, "Board" );
		BoardRenderer = boardObject.Components.GetOrCreate<SkinnedModelRenderer>();
		BoardRenderer.Model ??= Model.Load( BoardModel );
	}

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

			var renderer = child.Components.Get<ModelRenderer>();
			if ( renderer?.Model is null )
				continue;

			var modelName = renderer.Model.Name?.ToLowerInvariant() ?? string.Empty;
			if ( modelName.Contains( "helmet" ) || modelName.Contains( "hardhat" ) )
				return true;
		}

		return false;
	}

	public void Bail( BailType bailType = BailType.Normal )
	{
		if ( Bailed || !IsLocallyControlled() )
			return;

		TrickScores.Failed = true;
		Bailed = true;

		var hasHelmet = HasHelmet();
		var headPos = WorldPosition + WorldRotation.Up * 70f;

		if ( bailType == BailType.Landing )
		{
			if ( !hasHelmet )
			{
				PlaySound( "impact-bullet-flesh", headPos );
			}
			else
			{
				PlaySound( "helmet_hit", headPos );
			}

			var to = headPos + Vector3.Down * 50f;
			var headTr = Scene.Trace.Ray( headPos, to ).WithAnyTags( "solid", "player", "npc" ).Run();
			if ( headTr.Hit )
			{
				// Decal placement can be added back if needed.
			}
		}

		if ( bailType != BailType.Bail )
			PlaySound( "body_fall", WorldPosition );

		Head.Enabled = false;


		CreateRagdoll();

		BodyRenderer.Enabled = false;
		Controller.Enabled = false;
		Animator.Enabled = false;
		BoardRenderer.Enabled = false;

		_timeBailed = 0f;
	}

	public void CreateRagdoll()
	{
		if ( BodyRenderer is null )
			return;

		CreateBoardRagdoll();

		var ragdollObject = BodyRenderer.GameObject.Clone(
			BodyRenderer.WorldTransform,
			Game.ActiveScene,
			startEnabled: false,
			name: $"{GameObject.Name}_ragdoll"
		);
		Ragdoll = ragdollObject;
		DisableRagdollDressers( ragdollObject );
		ragdollObject.Enabled = true;
		SetClothingEnabled( BodyRenderer.GameObject, false );
		SetClothingEnabled( ragdollObject, true );

		var bodyModel = ragdollObject.Components.Get<SkinnedModelRenderer>();
		if ( bodyModel is null )
			return;

		ragdollObject.DestroyAsync( 5f );
		bodyModel.UseAnimGraph = false;

		foreach ( var skinnedModelRenderer in bodyModel.Components.GetAll<SkinnedModelRenderer>() )
		{
			skinnedModelRenderer.RenderType = ModelRenderer.ShadowRenderType.On;
		}

		bodyModel.Tags.Add( "ragdoll" );

		var modelPhysics = bodyModel.GetOrAddComponent<ModelPhysics>();
		modelPhysics.Model = bodyModel.Model;
		modelPhysics.Renderer = bodyModel;
		modelPhysics.MotionEnabled = true;

		foreach ( var body in modelPhysics.Bodies )
		{
			body.Component.Velocity += Velocity;
		}
	}

	private void RespawnAfterBail()
	{
		RealRotation = MathLD.FromToRotation( Vector3.Up * RealRotation, Vector3.Up ) * RealRotation;
		WorldRotation = RealRotation;

		var respawn = RespawnHelper.FindRespawnPosition( WorldPosition );
		if ( respawn.Found )
		{
			WorldPosition = respawn.Location;
			Respawn();
			return;
		}

		Respawn();
	}

	public void Respawn()
	{
		var spawn = Scene.GetAllComponents<SpawnPoint>().OrderBy( _ => Guid.NewGuid() ).FirstOrDefault();
		if ( spawn is not null )
		{
			WorldTransform = spawn.WorldTransform.WithPosition( spawn.WorldPosition + Vector3.Up * 50f );
		}

		Controller.Enabled = true;
		Animator.Enabled = true;
		BodyRenderer.Enabled = true;
		BoardRenderer.Enabled = true;
		Head.Enabled = true;

		Bailed = false;
		_timeBailed = 0f;
		Velocity = 0f;
		Ragdoll = null;
		BoardRagdoll = null;
		SetClothingEnabled( BodyRenderer.GameObject, true );
		Controller.ResetState();
	}

	private void CreateBoardRagdoll()
	{
		if ( !BoardRenderer.IsValid() )
			return;

		var boardObject = BoardRenderer.GameObject;
		var boardRagdoll = boardObject.Clone(
			boardObject.WorldTransform,
			Game.ActiveScene,
			startEnabled: false,
			name: $"{boardObject.Name}_ragdoll"
		);

		BoardRagdoll = boardRagdoll;

		var boardRenderer = boardRagdoll.Components.Get<SkinnedModelRenderer>();
		if ( boardRenderer.IsValid() )
		{
			boardRenderer.UseAnimGraph = false;
			boardRenderer.BoneMergeTarget = null;
			var ragdollModel = Model.Load( "models/skateboard.vmdl" );
			boardRenderer.Model = ragdollModel;
		}

		var collider = boardRagdoll.Components.GetOrCreate<ModelCollider>();
		if ( boardRenderer.IsValid() )
			collider.Model = boardRenderer.Model;

		var body = boardRagdoll.Components.GetOrCreate<Rigidbody>();
		body.MotionEnabled = true;
		body.Velocity = Velocity;

		boardRagdoll.Enabled = true;
		boardRagdoll.DestroyAsync( 5f );
	}

	private static void SetClothingEnabled( GameObject root, bool enabled )
	{
		if ( root is null )
			return;

		foreach ( var child in root.GetAllObjects( false ) )
		{
			if ( child.Tags.Has( "clothing" ) )
				child.Enabled = enabled;
		}
	}

	private static void DisableRagdollDressers( GameObject root )
	{
		if ( root is null )
			return;

		foreach ( var dresser in root.Components.GetAll<Dresser>( FindMode.EverythingInSelfAndDescendants ) )
		{
			dresser.Enabled = false;
		}

		foreach ( var stripper in root.Components.GetAll<Skateboard.ClothingModelSuffixStripper>( FindMode.EverythingInSelfAndDescendants ) )
		{
			stripper.Enabled = false;
		}
	}

	private void OnComboFinish()
	{
		ComboFinishClient();
		ClientTrickUpdate();
	}

	private void OnComboFail()
	{
		ClientTrickUpdate();
	}

	[Rpc.Owner]
	private void ComboFinishClient()
	{
		Sound.Play( "combo_finish", WorldPosition );
	}

	[Rpc.Owner]
	private void ClientTrickUpdate()
	{
		if ( GameObject.Network.IsOwner )
			TrickScoreHolder.OnLocalTrickScoreUpdate?.Invoke();
	}

	public void PlaySound( string eventName, Vector3 position )
	{
		if ( Networking.IsActive && !IsLocallyControlled() )
			return;

		PlaySoundRpc( eventName, position );
	}

	private bool IsLocallyControlled()
	{
		if ( !Networking.IsActive )
			return true;

		if ( GameObject.Network.IsOwner )
			return true;

		return OwningConnectionId == Connection.Local.Id;
	}


	[Rpc.Broadcast( NetFlags.Unreliable )]
	private void PlaySoundRpc( string eventName, Vector3 position )
	{
		Sound.Play( eventName, position );
	}

	[ConCmd( "kill" )]
	public static void Kill()
	{
		Local.Bail();
	}
}