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 );
}
}