using Sandbox.Movement; namespace Sandbox; [Expose] [Icon( "event_seat" ), Group( "Game" ), Title( "Chair" )] public partial class BaseChair : Component, Component.IPressable, ISitTarget { [Expose] public enum AnimatorSitPose { Standing = 0, Chair = 1, ChairForward = 2, ChairCrossed = 3, KneelingOpen = 4, Kneeling = 5, Ground = 6, GroundCrossed = 7, } /// /// A GameObject representing the seat position /// [Header( "Seat" )] [Property] public GameObject SeatPosition { get; set; } /// /// The sitting pose to use when a player is seated /// [Header( "Animation" )] [Property] public AnimatorSitPose SitPose { get; set; } = AnimatorSitPose.Chair; /// /// Height offset for sitting position, from -1 (lowest) to 1 (highest) /// [Range( -1, 1 )] [Property] public float SitHeight { get; set; } = 0; /// /// A GameObject representing the eye position /// [Header( "Eyes" )] [Property] public GameObject EyePosition { get; set; } /// /// Pitch range for seated players /// [Property] public Vector2 PitchRange { get; set; } = new Vector2( -90, 70 ); /// /// Yaw range for seated players /// [Property] public Vector2 YawRange { get; set; } = new Vector2( -120, 120 ); [Header( "Exit" )] [Property] public GameObject[] ExitPoints { get; set; } /// /// Chair is usable if the player can enter /// public bool CanPress( IPressable.Event e ) { var player = e.Source as PlayerController; if ( player is null ) return false; return CanEnter( player ); } /// /// Called when the player has pressed to use the chair. /// Only called if CanPress returned true. /// public bool Press( IPressable.Event e ) { var player = e.Source as PlayerController; if ( player is null ) return false; EnterChair( player ); return true; } /// /// Called on the host to enter the chair. /// [Rpc.Host] private void EnterChair( PlayerController player ) { if ( player.Network.Owner != Rpc.Caller ) return; if ( !CanEnter( player ) ) return; using ( Rpc.FilterInclude( player.Network.Owner ) ) { // TODO - when https://github.com/Facepunch/sbox/issues/3270 is done // we should be able to set the parent here on the host and have it replicate // rather than telling the client to parent themselves! Sit( player ); } } /// /// Called on the client to place the player in the chair. /// [Rpc.Broadcast( NetFlags.HostOnly )] public void Sit( PlayerController player ) { var seatPos = SeatPosition ?? GameObject; player.Body.Enabled = false; player.ColliderObject.Enabled = false; player.GameObject.SetParent( seatPos, false ); player.GameObject.LocalTransform = global::Transform.Zero; } /// /// Called on the host to request leaving the chair. /// [Rpc.Host] public void AskToLeave( PlayerController player ) { if ( player.Network.Owner != Rpc.Caller ) return; if ( GetOccupant() != player ) return; if ( !CanLeave( player ) ) return; using ( Rpc.FilterInclude( player.Network.Owner ) ) { // TODO - when https://github.com/Facepunch/sbox/issues/3270 is done // we should be able to set the parent here on the host and have it replicate // rather than telling the client to parent themselves! Eject( player ); } } /// /// Return true if this player can leave the chair /// public virtual bool CanLeave( PlayerController player ) { return true; } /// /// Called on the client to eject the player from the chair. /// [Rpc.Broadcast( NetFlags.HostOnly )] public void Eject( PlayerController player ) { if ( GetOccupant() != player ) return; var exitPoint = FindBestExitPoint(); var seatPos = SeatPosition ?? GameObject; player.GameObject.SetParent( null, true ); player.WorldPosition = exitPoint; player.EyeAngles = WorldRotation.Inverse * player.EyeAngles; } /// /// Returns a position to place the player when they exit the chair. This searches /// through ExitPoints to find the best one, which is usually the one the player is most /// facing towards. /// public Vector3 FindBestExitPoint() { if ( ExitPoints == null || ExitPoints.Length == 0 ) return (SeatPosition ?? GameObject).WorldPosition; return ExitPoints.OrderByDescending( ScoreExitPoint ).First().WorldPosition; } private float ScoreExitPoint( GameObject exitPoint ) { var eyeForward = GetOccupant()?.EyeTransform.Forward ?? WorldTransform.Forward; var seatPos = (SeatPosition ?? GameObject).WorldPosition; var toExit = exitPoint.WorldPosition - seatPos; var forwardScore = Vector3.Dot( toExit.Normal, eyeForward ); // todo: Trace test? Make sure we're not going through the world? return forwardScore * 1000.0f; } /// /// Return true if this player can enter the chair /// public virtual bool CanEnter( PlayerController player ) { if ( player is null ) return false; if ( IsOccupied ) return false; return true; } /// /// Get the transform representing the eye position when seated /// public virtual Transform GetEyeTransform() { var seatPos = EyePosition ?? SeatPosition ?? GameObject; return seatPos.WorldTransform; } /// /// Returns true if the chair is currently occupied /// public bool IsOccupied => GetOccupant().IsValid(); /// /// Gets the player that is currently occupying the chair /// public PlayerController GetOccupant() => GetComponentInChildren(); /// /// Called to update the player's animator when seated /// public virtual void UpdatePlayerAnimator( PlayerController controller, SkinnedModelRenderer renderer ) { // Make sure the controller is aligned with the chair controller.LocalTransform = global::Transform.Zero; // Make sure the body rotation is zero renderer.LocalRotation = Rotation.Identity; // Update animation parameters renderer.Set( "sit", (int)SitPose ); renderer.Set( "sit_offset_height", SitHeight * 12.0f ); renderer.Set( "b_grounded", true ); renderer.Set( "b_climbing", false ); renderer.Set( "b_swim", false ); renderer.Set( "duck", false ); // Look in the direction the player is aiming var eyesForward = controller.EyeTransform.Forward; renderer.SetLookDirection( "aim_eyes", eyesForward, controller.AimStrengthEyes ); renderer.SetLookDirection( "aim_head", eyesForward, controller.AimStrengthHead ); renderer.SetLookDirection( "aim_body", eyesForward, controller.AimStrengthBody ); // Clamp the eye angles ClampEyes( controller ); } /// /// Clamps the eye angles of a seated player between the PitchRange and YawRange /// protected void ClampEyes( PlayerController controller ) { var ea = controller.EyeAngles; ea.pitch = MathX.Clamp( ea.pitch, PitchRange.x, PitchRange.y ); ea.yaw = MathX.Clamp( ea.yaw, YawRange.x, YawRange.y ); controller.EyeAngles = ea; } /// /// Calculates the eye transform for a seated player /// public virtual Transform CalculateEyeTransform( PlayerController controller ) { // clamp the player's eye angles ClampEyes( controller ); var seatEyeTx = GetEyeTransform(); var transform = new Transform(); transform.Position = seatEyeTx.Position; transform.Rotation = WorldRotation * controller.EyeAngles.ToRotation(); return transform; } /// /// Draws the player model sitting in the chair if it's selected /// protected override void DrawGizmos() { var selectedObject = Scene.Editor?.SelectedGameObject; if ( selectedObject == null ) return; if ( selectedObject != GameObject && selectedObject != SeatPosition && selectedObject != EyePosition ) return; var seatPos = (SeatPosition ?? GameObject).WorldTransform; var localSeatPos = GameObject.WorldTransform.ToLocal( seatPos ); var so = Gizmo.Draw.Model( "models/citizen/citizen.vmdl", localSeatPos.WithScale( 1 ) ); so.ColorTint = Color.White.WithAlpha( 0.6f ); so.SetAnimParameter( "sit", (int)SitPose ); so.SetAnimParameter( "sit_offset_height", SitHeight * 12.0f ); so.SetAnimParameter( "b_grounded", true ); so.SetAnimParameter( "b_climbing", false ); so.SetAnimParameter( "b_swim", false ); so.SetAnimParameter( "duck", false ); so.Update( RealTime.Delta * 10.0f ); } }