Game/PlayerAvatar.cs
namespace Monolith;

/// <summary>
/// The player's body: a Terry wearing whatever the local user has actually configured, plus the
/// third person camera that looks at him.
///
/// Two things are worth knowing about how this is wired.
///
/// **The rig stays at the eye position.** All the movement, aiming and muzzle code works in eye
/// space and none of it changes here. The body is a child hung DOWN from the rig by
/// <see cref="PlayerMovement.EyeHeight"/>, and the camera is a child pulled BACK from it. That
/// keeps a third person view from touching a single line of the movement or firing code.
///
/// **The clothing comes from the user, not from us.** `ClothingContainer.CreateFromLocalUser()`
/// reads the avatar the player set up in s&box itself, so every skin, hat and colourway they
/// own works with no per-item support on our side.
/// </summary>
public sealed class PlayerAvatar : Component
{
	/// <summary>The object holding the camera. Created by GameBootstrap, positioned here.</summary>
	[Property] public GameObject CameraObject { get; set; }

	/// <summary>How far behind the eye the camera sits.</summary>
	[Property] public float CameraDistance { get; set; } = 165f;

	/// <summary>How far above the eye, so the body does not sit in the middle of the crosshair.</summary>
	[Property] public float CameraHeight { get; set; } = 26f;

	/// <summary>Sideways offset. Over the shoulder keeps the crosshair clear of your own head.</summary>
	[Property] public float CameraShoulder { get; set; } = 34f;

	private GameObject bodyObject;
	private SkinnedModelRenderer body;

	private GameObject gunObject;
	private static Model gunModel;

	/// <summary>
	/// The player's own avatar, kept so the drone heads can wear it too. Built once: the call
	/// reads user data and is not something to do per drone per frame.
	/// </summary>
	private ClothingContainer clothing;

	private PlayerMovement movement;
	private Miner miner;

	/// <summary>
	/// Where bolts actually leave from: the tip of the barrel in Terry's right hand.
	///
	/// This exists because the muzzle used to be derived from the CAMERA
	/// (`camera.WorldPosition + forward*30 + right*14 + down*12`). In first person that reads as
	/// firing from just off-screen. In third person the camera is behind and to the right of the
	/// body, so every bolt visibly spawned in the bottom right corner of the screen with nothing
	/// attached to it.
	/// </summary>
	public Vector3 MuzzlePosition { get; private set; }

	/// <summary>False until the gun exists, so the Miner knows to fall back to the camera.</summary>
	public bool HasMuzzle { get; private set; }


	/// <summary>Yaw the body is currently facing. Turns to follow movement, not the camera.</summary>
	private float bodyYaw;

	protected override void OnStart()
	{
		movement = Components.Get<PlayerMovement>();
		miner = Components.Get<Miner>();

		bodyObject = new GameObject( true, "Terry" );
		bodyObject.NetworkMode = NetworkMode.Never;
		bodyObject.Parent = GameObject;

		body = bodyObject.AddComponent<SkinnedModelRenderer>();
		body.Model = Model.Load( "models/citizen/citizen.vmdl" );

		DressFromLocalUser();
		CreateGun();
	}

	private void CreateGun()
	{
		gunObject = new GameObject( true, "Drill Gun" );
		gunObject.NetworkMode = NetworkMode.Never;

		var renderer = gunObject.AddComponent<ModelRenderer>();
		renderer.Model = GetGunModel();
		renderer.Tint = new Color( 0.62f, 0.66f, 0.74f );

		// A little light at the business end, so the gun reads as the source of the bolts even
		// before one is fired.
		var light = gunObject.AddComponent<PointLight>();
		light.LightColor = new Color( 1f, 0.7f, 0.32f ) * 2.2f;
		light.Radius = 190f;
		light.Shadows = false;
	}

	/// <summary>
	/// Applies the player's own s&box avatar. Wrapped because a user with no avatar set, or a
	/// build where the clothing service is unavailable, must not take the whole rig down with it:
	/// an undressed Terry is a far better outcome than no player at all.
	/// </summary>
	private void DressFromLocalUser()
	{
		try
		{
			clothing = ClothingContainer.CreateFromLocalUser();
			clothing?.Apply( body );
		}
		catch ( Exception e )
		{
			Log.Warning( $"Could not load the local avatar, using a bare Terry: {e.Message}" );
		}
	}

	protected override void OnUpdate()
	{
		// Frozen while a blocking screen or the pause menu is up. See GameTime.
		if ( GameTime.Paused )
			return;

		if ( !body.IsValid() || !movement.IsValid() )
			return;

		// Hang the body from the eye down to the floor.
		bodyObject.LocalPosition = Vector3.Down * movement.EyeHeight;

		var velocity = movement.Velocity.WithZ( 0 );

		// The body turns to face where you are MOVING, not where you are looking, so a strafe
		// reads as a strafe. It only turns while there is real speed, otherwise standing still
		// and sweeping the mouse would spin him on the spot.
		if ( velocity.Length > 40f )
			bodyYaw = velocity.Normal.EulerAngles.yaw;
		else
			bodyYaw = WorldRotation.Angles().yaw;

		bodyObject.WorldRotation = Rotation.FromYaw( bodyYaw );

		var local = Rotation.FromYaw( bodyYaw ).Inverse * velocity;

		Anim( "move_x", local.x );
		Anim( "move_y", local.y );
		Anim( "move_z", movement.Velocity.z );
		Anim( "move_groundspeed", velocity.Length );
		Anim( "b_grounded", movement.IsGrounded );

		// AIMING. This is what makes the gun look held rather than stuck to him.
		//
		// Setting holdtype alone leaves the arms hanging at his sides: the citizen graph only
		// raises them when it has something to aim AT, which means aim_body plus a non-zero
		// aim_body_weight. Without those the pose is idle and a gun pointed at the crosshair
		// reads as a floating prop, which is exactly how it looked.
		//
		// Holdtype 1 is the one-handed pistol pose. Rifle (2) is two-handed and leaves the left
		// hand gripping empty air next to a gun that is only in the right.
		Anim( "holdtype", 1 );
		Anim( "holdtype_handedness", 0 );
		Anim( "aim_body_weight", 1f );

		var look = WorldRotation.Forward;

		AnimLook( "aim_body", look );
		AnimLook( "aim_head", look );
		AnimLook( "aim_eyes", look );
	}

	// Each parameter is set independently on purpose. They used to share one try block, so the
	// first name the graph did not recognise silently skipped every parameter after it, which is
	// how holdtype ended up never being applied at all.

	private void Anim( string name, float value )
	{
		try { body.Set( name, value ); } catch ( Exception ) { }
	}

	private void Anim( string name, int value )
	{
		try { body.Set( name, value ); } catch ( Exception ) { }
	}

	private void Anim( string name, bool value )
	{
		try { body.Set( name, value ); } catch ( Exception ) { }
	}

	private void AnimLook( string name, Vector3 direction )
	{
		try { body.SetLookDirection( name, direction ); } catch ( Exception ) { }
	}

	/// <summary>
	/// Camera placement runs in OnPreRender so it is guaranteed to happen after movement has
	/// finished writing the rig position for the frame. Doing it in OnUpdate leaves it at the
	/// mercy of component ordering, which shows up as a camera that judders a frame behind.
	/// </summary>
	protected override void OnPreRender()
	{
		PlaceGun();
		UpdateDrones();

		if ( !CameraObject.IsValid() )
			return;

		// Identity local rotation means the boom inherits the rig's full rotation, pitch
		// included, so looking up and down orbits the camera the way it should.
		// Source axes: +X forward, +Y left, +Z up. Negative Y is therefore the right shoulder.
		CameraObject.LocalRotation = Rotation.Identity;

		// The shake is ADDED here rather than written by the effect itself, because this line
		// runs every frame and would overwrite anything it wrote.
		CameraObject.LocalPosition = new Vector3( -CameraDistance, -CameraShoulder, CameraHeight )
			+ StageLostEffect.ShakeOffset;

		UpdateSpeedFov();
	}

	/// <summary>
	/// Widens the field of view with hop speed.
	///
	/// Devil Daggers gives a perfect hop an FOV increase as its reward (GOALS 6d), and it is the
	/// cheapest speed cue in games: you feel fast because the world stretches, with no HUD and no
	/// number. Eased rather than snapped, because a per-frame FOV jump reads as a glitch.
	/// </summary>
	private void UpdateSpeedFov()
	{
		var camera = CameraObject.Components.Get<CameraComponent>();

		if ( !camera.IsValid() || !movement.IsValid() )
			return;

		float wanted = Tuning.BaseFieldOfView + movement.HopCharge * Tuning.HopFovBoost;

		camera.FieldOfView = MathX.Lerp( camera.FieldOfView, wanted,
			MathF.Min( 1f, Time.Delta * 7f ) );
	}

	/// <summary>
	/// Puts the gun in Terry's right hand and points it where you are aiming.
	///
	/// Position comes from the hand bone but rotation does NOT: the animgraph decides where the
	/// hand is, which is nowhere near the crosshair. Taking the position from the skeleton and
	/// the aim from the player is what makes the bolt leave the barrel and arrive under the
	/// crosshair at the same time.
	/// </summary>
	private void PlaceGun()
	{
		if ( !gunObject.IsValid() || !body.IsValid() )
			return;

		var hand = WorldPosition;
		bool found = false;

		try
		{
			if ( body.TryGetBoneTransform( "hand_R", out var bone ) )
			{
				hand = bone.Position;
				found = true;
			}
		}
		catch ( Exception )
		{
			// Bone names belong to the citizen addon. Falling back is better than failing.
		}

		if ( !found )
		{
			// Roughly where a held hand sits: forward, right, and a little below the eye.
			var yaw = bodyObject.IsValid() ? bodyObject.WorldRotation : WorldRotation;
			hand = WorldPosition + yaw.Forward * 22f + yaw.Right * 14f + Vector3.Down * 16f;
		}

		var aim = miner.IsValid() ? miner.AimPoint : WorldPosition + WorldRotation.Forward * 1000f;
		var direction = (aim - hand);

		// Sits slightly forward of the wrist rather than dead on the bone, which is the
		// difference between held and skewered.
		gunObject.WorldPosition = hand;

		if ( direction.Length > 1f )
		{
			var aimRotation = Rotation.LookAt( direction.Normal );

			// Blended toward the aim rather than snapped to it. With aim_body driving the arm the
			// hand already points roughly at the target, so a hard snap fights the animation and
			// makes the gun swim inside the fist; easing lets the two agree.
			gunObject.WorldRotation = HasMuzzle
				? Rotation.Lerp( gunObject.WorldRotation, aimRotation,
					MathF.Min( 1f, Time.Delta * 18f ) )
				: aimRotation;

			gunObject.WorldPosition = hand + aimRotation.Forward * 3f;
		}

		// The barrel runs along local forward, so the muzzle is simply out along it. Scaled with
		// the model when it shrank.
		MuzzlePosition = gunObject.WorldPosition + gunObject.WorldRotation.Forward * 17f;
		HasMuzzle = true;
	}

	/// <summary>Keeps the arch of drones matching the upgrade and hanging above you.</summary>
	private void UpdateDrones()
	{
		var progress = PlayerProgress.Local;
		if ( !progress.IsValid() )
			return;

		int wanted = progress.DronesDisabled ? 0 : progress.DroneCount;

		MiningDrone.MatchPopulation( Scene, wanted );

		if ( MiningDrone.All.Count == 0 )
			return;

		var aim = miner.IsValid() ? miner.AimPoint : WorldPosition + WorldRotation.Forward * 1000f;

		// Yaw only. Passing the full rotation would tip the whole arch into the floor whenever
		// you looked down.
		float yaw = WorldRotation.Angles().yaw;

		for ( int i = 0; i < MiningDrone.All.Count; i++ )
			MiningDrone.All[i].Follow( WorldPosition, yaw, aim, i, MiningDrone.All.Count );
	}

	/// <summary>
	/// A blocky slab of a gun, built the same way as everything else here: a hand-rolled vertex
	/// buffer, so it matches the voxel look instead of importing a detailed weapon into a game
	/// made of cubes.
	/// </summary>
	private static Model GetGunModel()
	{
		if ( gunModel != null )
			return gunModel;

		var vb = new VertexBuffer();
		vb.Init( true );

		int index = 0;

		// Body, then a barrel running out along +X, then a grip hanging below.
		//
		// Roughly HALF the first version. Terry is 64 units tall and the original was a 44 unit
		// slab, which is a rifle the size of his torso. A hand-held thing has to be sized against
		// the hand, not against the shape you are shooting at.
		AddBox( vb, ref index, new Vector3( 2f, 0f, 0f ), new Vector3( 13f, 4.5f, 5.5f ) );
		AddBox( vb, ref index, new Vector3( 12f, 0f, 0.8f ), new Vector3( 9f, 2.5f, 2.5f ) );
		AddBox( vb, ref index, new Vector3( -1f, 0f, -4.5f ), new Vector3( 4f, 3f, 6f ) );

		var mesh = new Mesh( Material.Load( "materials/default.vmat" ) );
		mesh.CreateBuffers( vb );

		gunModel = new ModelBuilder().AddMesh( mesh ).Create();
		return gunModel;
	}

	private static void AddBox( VertexBuffer vb, ref int index, Vector3 centre, Vector3 size )
	{
		Vector3[] normals =
		{
			Vector3.Forward, Vector3.Backward, Vector3.Left,
			Vector3.Right, Vector3.Up, Vector3.Down,
		};

		foreach ( var n in normals )
		{
			var reference = MathF.Abs( n.z ) > 0.9f ? Vector3.Forward : Vector3.Up;
			var u = Vector3.Cross( n, reference ).Normal;
			var v = Vector3.Cross( n, u ).Normal;

			var face = centre + n * 0.5f * Project( size, n );

			var du = u * 0.5f * Project( size, u );
			var dv = v * 0.5f * Project( size, v );

			var p0 = face - du - dv;
			var p1 = face + du - dv;
			var p2 = face + du + dv;
			var p3 = face - du + dv;

			vb.Add( new Vertex( p0, n, u, new Vector4( 0, 0, 0, 0 ) ) );
			vb.Add( new Vertex( p1, n, u, new Vector4( 1, 0, 0, 0 ) ) );
			vb.Add( new Vertex( p2, n, u, new Vector4( 1, 1, 0, 0 ) ) );
			vb.Add( new Vertex( p3, n, u, new Vector4( 0, 1, 0, 0 ) ) );

			vb.AddRawIndex( index + 0 ); vb.AddRawIndex( index + 1 ); vb.AddRawIndex( index + 2 );
			vb.AddRawIndex( index + 0 ); vb.AddRawIndex( index + 2 ); vb.AddRawIndex( index + 3 );

			index += 4;
		}
	}

	private static float Project( Vector3 size, Vector3 axis )
		=> MathF.Abs( axis.x ) * size.x + MathF.Abs( axis.y ) * size.y + MathF.Abs( axis.z ) * size.z;

	protected override void OnDestroy()
	{
		gunObject?.Destroy();
		gunObject = null;

		MiningDrone.MatchPopulation( Scene, 0 );
	}
}