Player/PlayerPawn.cs

Component for a player pawn. It initializes player visuals, movement and inventory, handles teleporting, camera/sound ticks for local players, fixed-update physics including input, movement and dummy AI, and sets up components declared on a character definition.

Networking
using KOTH.UI;
using Sandbox;
using Sandbox.Citizen;
using Sandbox.Diagnostics;
using Sandbox.Events;
using Sandbox.Services;
using System.Reflection.Metadata.Ecma335;
using KOTH.PlayerExp;

namespace KOTH;

public sealed partial class PlayerPawn : Component, IDescription
{
	[Property, Sync(SyncFlags.FromHost)] public FPlayerPawnDefinition PlayerPawnDefinition { get; private set; }
	public string DisplayName { get; private set; } = "UNINITALIZED";

	public void SetPlayerPawnDefinition(FPlayerPawnDefinition PlayerPawnDefinitionIn)
	{
		Assert.True(Networking.IsHost);

		PlayerPawnDefinition = PlayerPawnDefinitionIn;
		IsDummy = PlayerPawnDefinitionIn.IsDummy;
		Team = PlayerPawnDefinitionIn.Team;
	}

	//////////////////////////////////////////////////////////////////////////////////

	[Property] public PlayerBody Body { get; set; }
	[Property] public GameObject Head { get; set; }
	[Property] public GameObject GibPrefab { get; set; } // TODO : this should be in character def
	[Property] public CitizenAnimationHelper AnimationHelper { get; set; }
	[Property] public BoxCollider PlayerBoxCollider { get; set; }
	[Property] public Team DisplayTeam { get => Team; }

	//////////////////////////////////////////////////////////////////////////////////

	[RequireComponent] public DamageComponent DamageComponent { get; private set; }
	[RequireComponent] public TagBinder TagBinder { get; private set; }
	[RequireComponent] public CharacterController CharacterController { get; private set; }
	[RequireComponent] public HighlightOutline Outline { get; private set; }
	[RequireComponent] public PlayerInventory Inventory { get; private set; }

	//////////////////////////////////////////////////////////////////////////////////

	[Property] public bool IsDummy { get; private set; } = false;
	[Property] public bool IsLocallyControlled => !IsProxy && !IsDummy;
	[Property] public bool IsViewer => PlayerState.Local?.PlayerPawn == this; // TODO : make spectate target in playerpawn?

	// TODO : dummy only var, refactor
	public bool IsJumper = false;
	public bool IsWalker = false;

	//////////////////////////////////////////////////////////////////////////////////

	[Sync(SyncFlags.FromHost)] public TimeSince TimeSinceLastRespawn { get; private set; }
	public Team Team { get; set; } = Team.Unassigned;
	public Action OnPlayerStart;

	public void Teleport(Transform transform)
	{
		Teleport(transform.Position, transform.Rotation);
	}

	[Rpc.Owner]
	public void Teleport(Vector3 position, Rotation rotation)
	{
		Transform.World = new(position, rotation);
		Transform.ClearInterpolation();
		EyeAngles = rotation.Angles();

		if (CharacterController.IsValid())
		{
			CharacterController.Velocity = Vector3.Zero;
			CharacterController.IsOnGround = true;
		}
	}

	//////////////////////////////////////////////////////////////////////////////////

	protected override void OnStart()
	{
		Assert.NotNull(Head);
		Assert.True(PlayerPawnDefinition.IsValid());

		// top 1 top3 lines
		Head.LocalPosition = Head.LocalPosition.WithZ(64 * PlayerBody.Heights[PlayerPawnDefinition.CharacterDefinition.Height]);

		DisplayName = PlayerPawnDefinition.Name;
		GameObject.Name = DisplayName;

		Tags.Add($"{Team}");
		Team = PlayerPawnDefinition.Team;

		CharacterDefinition CharacterDefinition = PlayerPawnDefinition.CharacterDefinition;
		Assert.True(SetMovementVariables(CharacterDefinition));

		foreach (Type ComponentType in CharacterDefinition.SpecificComponents)
		{
			if (ComponentType.IsSubclassOf(typeof(Component)))
			{
				var Type = TypeLibrary.GetType(ComponentType);
				GameObject.Components.Create(Type);
			}
		}

		if (IsLocallyControlled)
		{
			CreatePlayerCamera();
			EyeAngles = WorldRotation.Angles();

			Tags.Add("self");

			Inventory.Give(CharacterDefinition.SecondaryWeapon, false);
			Inventory.Give(CharacterDefinition.PrimaryWeapon, true);

			Mouse.Visibility = MouseVisibility.Hidden;
		}
		else
		{
			// HACK
			Tags.Remove("self");
		}

		// TODO : load in data in a nicer way?
		if (Networking.IsHost)
		{
			DamageComponent.Initalize(CharacterDefinition.MaxHealth, Team);
		}

		OnPlayerStart?.Invoke();
	}

	protected override void OnUpdate()
	{
		if (IsLocallyControlled)
		{
			CameraTick();
			SoundTick();
		}

		UpdateCrouch();
		TickVFXs();
	}

	public SceneTraceResult CachedEyeTrace { get; private set; }
	protected override void OnFixedUpdate()
	{
		// TODO : these have been downgraded til sbox has a proper component
		// system (or has a fixed update that waits til its components oneanbled is fired(?))
		//Assert.True(CharacterController.IsValid());
		//Assert.True(DamageComponent.IsValid());

		if (!CharacterController.IsValid() || !DamageComponent.IsValid())
		{
			Log.Warning("shitbox strikes again!");
			return;
		}

		var wasGrounded = IsGrounded;
		IsGrounded = CharacterController.IsOnGround;

		if (IsGrounded != wasGrounded)
		{
			GroundedChanged(wasGrounded, IsGrounded);
		}

		UpdateZones();

		if (IsDummy && IsAlive)
		{
			DoDummyMovement();
			return; // NOTE : early return
		}

		if (!IsAlive || !IsLocallyControlled)
		{
			return; // NOTE : early return
		}

		if (IsViewer)
		{
			CachedEyeTrace = Scene.Trace.Ray(AimRay, 100000f)
				.IgnoreGameObjectHierarchy(GameObject)
				.WithoutTags("ragdoll", "movement")
				.UseHitboxes()
				.Run();
		}

		_previousVelocity = CharacterController.Velocity;

		BuildWishInput();
		BuildWishVelocity();
		BuildInput();

		ApplyAcceleration();
		ApplyMovement();
		DebugUpdate();
	}

	//////////////////////////////////////////////////////////////////////////////////

	private void DoDummyMovement()
	{
		if (CharacterController.IsOnGround && IsJumper)
		{
			IsCrouching = true;
			CharacterController.Punch(Vector3.Up * JumpPower);
			BroadcastPlayerJumped();
		}

		if (IsWalker)
		{
			WishMove = WorldRotation.Forward;
			BuildWishVelocity();
		}

		ApplyAcceleration();
		ApplyMovement();
	}
}