Map/FuncDoor.cs
[EditorHandle( Icon = "touch_app" )]
public sealed class FuncDoor : BaseToggle, Component.IPressable
{
	public delegate Task ButtonDelegate( GameObject presser );

	[Property] public ButtonDelegate OnButtonPressed { get; set; }
	[Property] public ButtonDelegate OnButtonReleased { get; set; }

	[Property, Group( "Opening" ), Order( 1 )] public Action OnOpenStart { get; set; }
	[Property, Group( "Opening" ), Order( 1 )] public Action OnOpenEnd { get; set; }
	[Property, Group( "Opening" ), Order( 1 )] public float OpenDuration { get; set; } = 1.0f;
	[Property, Group( "Opening" ), Order( 1 )] public Curve OpenMovementCurve { get; set; } = new Curve( new Curve.Frame( 0, 0 ), new Curve.Frame( 1, 1 ) );

	[Property, Group( "Closing" ), Order( 2 )] public Action OnCloseStart { get; set; }
	[Property, Group( "Closing" )] public Action OnCloseEnd { get; set; }
	[Property, Group( "Closing" )] public float CloseDuration { get; set; } = 1.0f;
	[Property, Group( "Closing" )] public Curve CloseMovementCurve { get; set; } = new Curve( new Curve.Frame( 0, 0 ), new Curve.Frame( 1, 1 ) );

	[Property] public bool AutoReset { get; set; } = true;
	[Property, ShowIf( "AutoReset", true )] public float ResetTime { get; set; } = 1.0f;

	[Property, Group( "Movement" )] public Vector3 Pivot { get; set; }
	[Property, Group( "Movement" )] public Angles TargetRotation { get; set; } = new Angles( 0, 90, 0 );
	[Property, Group( "Movement" )] public bool OpenAwayFromPlayer { get; set; } = false;

	Transform initialTransform;

	[Sync] public bool IsMoving { get; set; }

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

		initialTransform = LocalTransform;

		// initial state pos
		if ( State ) LocalTransform = initialTransform.RotateAround( initialTransform.PointToWorld( Pivot ), TargetRotation );
		else LocalTransform = initialTransform;
	}
	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		if ( !Gizmo.IsSelected )
			return;

		using ( Gizmo.Scope( "Tool", new Transform( Pivot ) ) )
		{
			Gizmo.Hitbox.DepthBias = 0.1f;

			if ( Gizmo.Control.Position( "pivot", 0, out var newPivot ) )
			{
				Pivot += newPivot;
			}
		}

		var delta = MathF.Sin( RealTime.Now * 2.0f ).Remap( -1, 1 );
		DrawAt( delta );
		DrawAt( 0 );
		DrawAt( 1 );
	}

	void DrawAt( float f )
	{
		Gizmo.Transform = WorldTransform.RotateAround( WorldTransform.PointToWorld( Pivot ), TargetRotation * f );

		var bbox = GameObject.GetLocalBounds();

		Gizmo.Draw.IgnoreDepth = false;
		Gizmo.Draw.Color = Color.Yellow;
		Gizmo.Draw.LineThickness = 3;
		Gizmo.Draw.LineBBox( bbox );
		Gizmo.Draw.IgnoreDepth = true;

		Gizmo.Draw.LineThickness = 1;
		Gizmo.Draw.Color = Gizmo.Draw.Color.WithAlpha( 0.3f );
		Gizmo.Draw.LineBBox( bbox );
	}

	[Rpc.Broadcast]
	public void Press( GameObject presser )
	{
		if ( presser.Network.Owner != Rpc.Caller )
			return;

		if ( IsMoving )
			return;

		OnButtonPressed?.Invoke( presser );

		if ( IsProxy )
			return;

		if ( State )
		{
			if ( !AutoReset ) Close();
			return;
		}

		Open( presser );
	}

	async void Open( GameObject presser )
	{
		OnOpenStart?.Invoke();
		IsMoving = true;

		var doorForward = LocalTransform.Rotation.Forward;
		var toPlayer = (presser.WorldPosition - LocalTransform.Position).Normal;

		var finalRotation = TargetRotation;

		bool isBehind = Vector3.Dot( toPlayer, doorForward ) > 0;

		if ( OpenAwayFromPlayer && isBehind )
		{
			finalRotation = new Angles( -TargetRotation.pitch, -TargetRotation.yaw, -TargetRotation.roll );
		}

		await AnimateRotationTo( finalRotation, OpenMovementCurve, OpenDuration );

		State = true;
		OnOpenEnd?.Invoke();

		if ( AutoReset && ResetTime >= 0.0f )
		{
			await Task.DelaySeconds( ResetTime );
			Close();
		}

		IsMoving = false;
	}

	async void Close()
	{
		OnCloseStart?.Invoke();

		IsMoving = true;

		await AnimateRotationTo( Rotation.Identity, CloseMovementCurve, CloseDuration );

		State = false;
		IsMoving = false;

		OnCloseEnd?.Invoke();
	}

	async Task AnimateRotationTo( Rotation rot, Curve curve, float time )
	{
		float d = 0;
		var start = LocalTransform;
		var localPivot = start.PointToWorld( Pivot );

		var targetTx = initialTransform.RotateAround( LocalTransform.PointToWorld( Pivot ), rot );

		var targetRot = initialTransform.Rotation * rot;

		//rot = rot * start.Rotation.Inverse;

		while ( time > d )
		{
			var delta = d.Remap( 0, time );

			var t = curve.Evaluate( delta );

			var rotDelta = Rotation.Lerp( start.Rotation, targetRot, t );

			var tx = start.RotateAround( localPivot, rotDelta * start.Rotation.Inverse );

			LocalTransform = tx;

			d += Time.Delta;

			await Task.FrameEnd();
		}

		LocalTransform = targetTx;
	}

	[Rpc.Broadcast]
	public void Release( GameObject presser )
	{
		if ( presser.Network.Owner != Rpc.Caller )
			return;

		OnButtonReleased?.Invoke( presser );

		if ( IsProxy )
			return;
	}


	[ActionGraphNode( "example.addone" ), Pure]
	[Title( "GetPlayer" ), Group( "Examples" ), Icon( "exposure_plus_1" )]
	public static Player GetPlayer( object value = default )
	{
		if ( value is GameObject o )
		{
			return o.Components.Get<Player>();
		}

		return null;
	}

	bool IPressable.Press( IPressable.Event e )
	{
		if ( IsMoving )
			return false;

		Press( e.Source.GameObject );
		return true;
	}

	void IPressable.Release( IPressable.Event e )
	{
		Release( e.Source.GameObject );
	}

	bool IPressable.CanPress( Sandbox.Component.IPressable.Event e )
	{
		if ( IsMoving ) return false;
		return true;
	}
}