Player/CameraController.cs
using Sandbox.UI;

namespace PlanetMeat;

[Group( "Planet Meat - Player" ), Title( "Camera Controller" ), Icon( "photo_camera_front" )]
public sealed class CameraController : Component, IAgendaPhaseListener
{
	const float PAN_SPEED = 0.75f;
	const float ORBIT_SPEED = -35.0f;
	const float MIN_Z_DIST = 800.0f;
	const float MAX_Z_DIST = MIN_Z_DIST + ZOOM_STEP * 5;
	const float ZOOM_STEP = 500.0f;
	const float ZOOM_SPEED = 10.0f;
	const float SPEED_MODIFIER_STRENGTH = 2.0f;
	const float MIN_PITCH = -80f;
	const float MAX_PITCH = -10f;

	public static CameraController Local { get; private set; }
	private static Plane AimPlane { get; } = new( Vector3.Zero, Vector3.Up );

	public GameObject Armature;
	public Vector3 AimPosition { get; private set; } = Vector3.Zero;
	public Vector3 PreviousAimPosition { get; private set; } = Vector3.Zero;
	public float ViewYaw => Armature.WorldRotation.Angles().yaw;

	public Ray MouseRay => Scene.Camera.ScreenPixelToRay( Mouse.Position );

	[Property] private Vector2 Bounds { get; set; }

	private Panel _previousFocus;

	private float TargetZ { get; set; } = -1.0f;
	protected override void OnStart()
	{
		base.OnStart();

		Armature = GameObject.Parent;
		if ( !IsProxy )
			Local = this;
		Mouse.Visibility = MouseVisibility.Visible;
		TargetZ = MAX_Z_DIST;
	}

	private void ForceFocusToPassInputs( Panel focus )
	{
		if ( _previousFocus != focus )
		{
			if ( focus?.IsValid ?? false )
			{
				focus.ButtonInput = PanelInputType.Game;
				_previousFocus = focus;
			}
			else
			{
				_previousFocus = null;
			}
		}
	}

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

		ForceFocusToPassInputs( InputFocus.Current );

		if ( Input.Pressed( "ZoomIn" ) )
			TargetZ -= ZOOM_STEP;
		else if ( Input.Pressed( "ZoomOut" ) )
			TargetZ += ZOOM_STEP;
		TargetZ = TargetZ.Clamp( MIN_Z_DIST, MAX_Z_DIST );
		if ( !LocalPosition.z.AlmostEqual( TargetZ ) )
			LocalPosition = LocalPosition.LerpTo( LocalPosition.WithZ( TargetZ ), ZOOM_SPEED * Time.Delta );

		var angles = Armature.WorldRotation.Angles();
		var movt = Input.AnalogMove.RotateAround( Vector3.Zero, new Angles( 0.0f, angles.yaw, 0.0f ) ).ClampLength( 1.0f );
		if ( Input.Down( "Faster" ) )
			movt *= SPEED_MODIFIER_STRENGTH;
		else if ( Input.Down( "Slower" ) )
			movt /= SPEED_MODIFIER_STRENGTH;

		var nextPosition = Armature.WorldPosition + movt * PAN_SPEED * Time.Delta * LocalPosition.z;
		if ( Bounds.x > 0 )
			nextPosition.x = nextPosition.x.Clamp( -Bounds.x, Bounds.x );
		if ( Bounds.y > 0 )
			nextPosition.y = nextPosition.y.Clamp( -Bounds.y, Bounds.y );
		Armature.WorldPosition = nextPosition;

		if ( Input.Down( "CamRotate" ) )
			Armature.WorldRotation = angles
				.WithYaw( angles.yaw + ORBIT_SPEED * Mouse.Delta.x * Time.Delta )
				.WithPitch( (angles.pitch - ORBIT_SPEED * Mouse.Delta.y * Time.Delta).Clamp( MIN_PITCH, MAX_PITCH ) );

		PreviousAimPosition = AimPosition;
		AimPosition = AimPlane.IntersectRay( MouseRay ) ?? Vector3.Zero;

		var tool = PlayerTool.Selected;
		if ( tool is null )
			return;

		tool.OnHover( AimPosition );

		if ( Input.Pressed( "Place" ) )
		{
			tool.OnPlaceStarted( AimPosition );
		}
		else
		{
			var placeDown = Input.Down( "Place" );
			var placeReleased = Input.Released( "Place" );
			if ( placeDown || placeReleased )
				tool.OnPlaceUpdated( PreviousAimPosition, AimPosition );
			if ( placeReleased )
				tool.OnPlaceEnded( AimPosition );
		}

		if ( Input.Pressed( "Cancel" ) )
			tool.OnCancel( AimPosition );
	}

	public void OnTimelinePhase( Agenda.Phases _prev, Agenda.Phases _current )
	{
		PlayerTool.Selected?.EndUse();
	}
}