Player.cs
using System;

namespace GuessIt;

public sealed class Player : Component, Component.INetworkSpawn
{
	[Sync( SyncFlags.FromHost )] public int Score { get; set; } = 0;
	[Sync( SyncFlags.FromHost )] public bool HasGuessed { get; set; } = false;

	[Sync] public float ColorHue { get; set; } = 0.0f;

	[Property] public SkinnedModelRenderer Body { get; set; }
	[Property] public CameraComponent Camera { get; set; }
	[Property] public Voice Voice { get; set; }

	public Texture CameraTexture;
	public Color Color => new ColorHsv( ColorHue, 0.4f, 1f ).ToColor();
	public bool IsDrawing => GameMenu.Instance?.CurrentlyDrawing == this;

	public static Player Local => Game.ActiveScene.GetAllComponents<Player>().FirstOrDefault( x => x.Network.OwnerId == Connection.Local.Id );

	TimeSince _timeSinceCreated;
	bool _isIdle;

	Dictionary<string, float> HappyMorph => new()
		{
			{ "innerbrowraiserL", 1f },
			{ "innerbrowraiserR", 1f },
			{ "outerbrowraiserL", 0.5f },
			{ "outerbrowraiserR", 0.5f },
			{ "cheekinflateL", 0.1f },
			{ "cheekinflateR", 0.1f },
			{ "openjawL", 0.25f },
			{ "openjawR", 0.25f },
			{ "lipcornerpullerL", 1f },
			{ "lipcornerpullerR", 1f },
			{ "openmouthL", 1f },
			{ "openmouthR", 1f }
		};

	Dictionary<string, float> AngryMorph => new()
		{
			{ "browlowererL", 1f },
			{ "browlowererR", 1f },
			{ "eyelidbuldgelower", 1f },
			{ "innerbrowraiserL", 1f },
			{ "innerbrowraiserR", 1f },
			{ "nosewrinklerL", 1f },
			{ "nosewrinklerR", 1f },
			{ "jawsuck", 1f },
			{ "lipcornerdepresserL", 1f },
			{ "lipcornerdepresserR", 1f },
			{ "openjawL", 0.8f },
			{ "openjawR", 0.8f },
			{ "upperLipFunneler", 0.5f },
			{ "upperlipraiserL", 1f },
			{ "upperlipraiserR", 1f }
		};

	int[] WinningPoses => new int[] { 1, 3, 4, 5, 6 };
	int[] LosingPoses => new int[] { 2, 7 };

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

		var idTag = Id.ToString().Substring( 0, 12 );
		GameObject.Tags.Add( idTag );
		Camera.RenderTags.Add( "light" );
		Camera.RenderTags.Add( idTag );

		Body.GameObject.Enabled = false;
		_timeSinceCreated = -Random.Shared.Float( 0, 2f );
	}

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

		if ( !IsProxy )
		{
			ColorHue = Random.Shared.Float( 0, 360f );
			GameMenu.Instance?.RequestCanvas();
		}
		CameraTexture = Texture.CreateRenderTarget( $"av_{Network.Owner.Id}", ImageFormat.RGBA8888, new( 128, 128 ) );
	}

	public void OnNetworkSpawn( Connection owner )
	{
		var clothing = new ClothingContainer();
		clothing.Deserialize( owner.GetUserData( "avatar" ) );
		if ( clothing.PrefersHuman )
		{
			Body.Model = Model.Load( "models/citizen_human/citizen_human_male.vmdl" );
		}
		clothing.Apply( Body );
		Camera.LocalPosition = new Vector3( 32, 0, 112 * clothing.Height );
	}

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

		var isSeen = _timeSinceCreated >= 0f;
		if ( isSeen && !_isIdle )
		{
			Body.Set( "special_idle_states", 1 );
			Body.GameObject.Enabled = true;
			_isIdle = true;
			SetPose( 0 );
		}

		if ( Camera.IsValid() && CameraTexture is not null )
		{
			Camera.OrthographicHeight = 256;
			Camera.RenderToTexture( CameraTexture );
		}

		Voice.Enabled = (HasGuessed == Local.HasGuessed) || !IsDrawing || (IsDrawing && Local.HasGuessed);
	}

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

		CameraTexture?.Dispose();
		CameraTexture = null;
	}

	public void ApplyMorph( Dictionary<string, float> morphs )
	{
		foreach ( var name in Body.Morphs.Names )
		{
			Body.Morphs.Clear( name );
		}
		if ( morphs is null ) return;
		foreach ( var morph in morphs )
		{
			Body.Morphs.Set( morph.Key, morph.Value );
		}
	}

	public void SetPose( int pose )
	{
		Body.Set( "special_idle_states", pose == 0 ? 1 : 0 );
		Body.Set( "holdtype_pose", pose );
	}

	public void ResetPose()
	{
		_timeSinceCreated = -Random.Shared.Float( 0, 2f );
		_isIdle = false;
		ApplyMorph( null );
	}

	public void SetWinPose()
	{
		var pose = WinningPoses[Random.Shared.Next( 0, WinningPoses.Length )];
		SetPose( pose );
		ApplyMorph( HappyMorph );
	}

	public void SetLosePose()
	{
		var pose = LosingPoses[Random.Shared.Next( 0, LosingPoses.Length )];
		SetPose( pose );
		ApplyMorph( AngryMorph );
	}

	[Rpc.Broadcast]
	public void BroadcastResetPose()
	{
		ResetPose();
	}

	[Rpc.Broadcast]
	public void BroadcastHappyMorph()
	{
		SetWinPose();
	}

	[Rpc.Broadcast]
	public void BroadcastAngryMorph()
	{
		SetLosePose();
	}
}