Weapons/ToolGun/Modes/Hydraulic/LengthConstraintEntity.cs
using Sandbox.Utility;
/// <summary>
/// A length-changing constraint: a SliderJoint whose length is puppeted between MinLength
/// and MaxLength by player input, wires, or an idle animation loop. Hydraulic, Muscle and
/// Winch are the same component with different presentation and defaults - <see cref="Kind"/>
/// just picks which one this instance is, for undo names, tool info and wiring titles.
/// </summary>
public sealed class LengthConstraintEntity : Component, IPlayerControllable
{
public enum ConstraintKind
{
Hydraulic,
Muscle,
Winch
}
[Property]
public ConstraintKind Kind { get; set; }
[Property, Range( 0, 1 )]
public GameObject OnEffect { get; set; }
[Property, Range( 0, 100 ), ClientEditable]
public float MinLength { get; set; } = 10f;
[Property, Range( 0, 100 ), ClientEditable]
public float MaxLength { get; set; } = 100f;
[Property, Range( 0, 1 ), ClientEditable]
public float Length { get; set; } = 0.5f;
[Property, Range( 0, 1 ), ClientEditable]
public float Speed { get; set; } = 0.25f;
[Property, ClientEditable]
public ClientInput Push { get; set; }
[Property, Range( 0, 1 ), ClientEditable]
public float PushSpeed { get; set; } = 0.25f;
[Property, ClientEditable]
public ClientInput Pull { get; set; }
[Property, Range( 0, 1 ), ClientEditable]
public float PullSpeed { get; set; } = 0.25f;
[Property, ClientEditable]
public ClientInput Toggle { get; set; }
/// <summary>
/// While the input is active we'll extend towards full length
/// </summary>
[Property, ClientEditable]
public ClientInput Activate { get; set; }
[Property]
public SliderJoint Joint { get; set; }
[Property, ClientEditable, ToggleGroup( "Animated" )]
public bool Animated { get; set; }
[Property, ClientEditable, ToggleGroup( "Animated" ), Range( 0, 10 )]
public float AnimationSpeed { get; set; } = 1.0f;
[Property, ClientEditable, ToggleGroup( "Animated" )]
public EaseType EaseIn { get; set; } = EaseType.Linear;
[Property, ClientEditable, ToggleGroup( "Animated" )]
public EaseType EaseOut { get; set; } = EaseType.Linear;
/// <summary>
/// Emits the current 0-1 extension whenever it changes. Wire it into a gauge, a
/// dependent mechanism, or another length constraint's Push/Pull to gang two rams together.
/// </summary>
[SignalOutput]
public SignalOutput Position { get; set; } = new();
public enum EaseType
{
Linear,
EaseIn,
EaseOut,
EaseInOut,
Bounce
}
protected override void OnEnabled()
{
base.OnEnabled();
OnEffect?.Enabled = false;
}
bool _state;
public void SetActiveState( bool state )
{
if ( _state == state ) return;
_state = state;
OnEffect?.Enabled = state;
Network.Refresh();
}
float? _lastTargetValue;
float? _targetValue;
float _lastEmitted = -1f;
protected override void OnFixedUpdate()
{
if ( !Networking.IsHost ) return;
if ( _targetValue.HasValue )
{
if ( _targetValue > Length )
{
Length += PushSpeed * Time.Delta * 5.0f;
if ( Length > 1 )
{
_targetValue = null;
}
}
else
{
Length -= PullSpeed * Time.Delta * 5.0f;
if ( Length < 0 )
{
_targetValue = null;
}
}
}
Length = Length.Clamp( 0, 1 );
if ( !_lastEmitted.AlmostEqual( Length, 0.001f ) )
{
_lastEmitted = Length;
Position.Emit( this, Length );
}
}
[SignalInput( Id = nameof( Push ), Default = true )]
public void PushSignal( float amount ) => PushLength( amount );
[SignalInput( Id = nameof( Pull ) )]
public void PullSignal( float amount ) => PullLength( amount );
[SignalInput( Id = nameof( Toggle ) )]
public void ToggleSignal() => ToggleTarget();
[SignalInput( Id = nameof( Activate ) )]
public void ActivateSignal( SignalEvent value ) => ActivateLength( value.Analog, value.Released );
public void OnControl()
{
PushLength( Push.GetAnalog() );
PullLength( Pull.GetAnalog() );
ActivateLength( Activate.GetAnalog(), Activate.Released() );
if ( Toggle.Pressed() ) ToggleTarget();
}
private void PushLength( float amount )
{
if ( Networking.IsHost && amount > 0.5f )
Length = (Length + PushSpeed * Time.Delta * 5f).Clamp( 0f, 1f );
}
private void PullLength( float amount )
{
if ( Networking.IsHost && amount > 0.5f )
Length = (Length - PullSpeed * Time.Delta * 5f).Clamp( 0f, 1f );
}
private void ActivateLength( float amount, bool released )
{
if ( !Networking.IsHost ) return;
if ( amount > 0.5f ) Length = (Length + Speed * Time.Delta).Clamp( 0f, 1f );
else if ( released ) Length = 0f;
}
private void ToggleTarget()
{
_targetValue = _lastTargetValue.HasValue ? (_lastTargetValue > 0.5f ? 0f : 1f) : 1f;
_lastTargetValue = _targetValue;
}
float _animTime = 0;
protected override void OnUpdate()
{
base.OnUpdate();
if ( !Joint.IsValid() ) return;
var line = Joint.Body.WorldPosition - Joint.GameObject.WorldPosition;
var line_rot = Rotation.LookAt( line, WorldRotation.Up );
DebugOverlay.Line( Joint.GameObject.WorldPosition, Joint.Body.WorldPosition, Color.Green );
if ( Animated )
{
_animTime += Time.Delta * AnimationSpeed * 0.33f;
_animTime = _animTime % 2;
var delta = _animTime;
if ( delta > 1 )
{
delta = 2 - delta;
delta = GetEase( 1 - delta, EaseOut );
delta = 1 - delta;
}
else
{
delta = GetEase( delta, EaseIn );
}
Length = (delta);
}
Joint.MinLength = MinLength + (Length * (MaxLength - MinLength));
Joint.MaxLength = MinLength + (Length * (MaxLength - MinLength));
if ( GetComponent<CapsuleCollider>() is CapsuleCollider capsule )
{
capsule.Static = false;
capsule.Start = capsule.WorldTransform.PointToLocal( Joint.GameObject.WorldPosition );
capsule.End = capsule.WorldTransform.PointToLocal( Joint.Body.WorldPosition );
capsule.Radius = 1.0f;
capsule.ColliderFlags = ColliderFlags.IgnoreMass;
capsule.Tags.Set( "trigger", true );
}
if ( GetComponent<SkinnedModelRenderer>() is SkinnedModelRenderer renderer )
{
renderer.CreateBoneObjects = true;
var len = line.Length - MinLength;
var a = Joint.GameObject.WorldPosition;
var b = a + line.Normal * MinLength * 0.5f;
var c = b + line.Normal * len;
var d = c + line.Normal * MinLength * 0.5f;
renderer.GetBoneObject( 0 )?.WorldTransform = new Transform( a, line_rot );
renderer.GetBoneObject( 0 )?.Flags |= GameObjectFlags.ProceduralBone;
renderer.GetBoneObject( 1 )?.WorldTransform = new Transform( b, line_rot ); ;
renderer.GetBoneObject( 1 )?.Flags |= GameObjectFlags.ProceduralBone;
renderer.GetBoneObject( 2 )?.WorldTransform = new Transform( c, line_rot ); ;
renderer.GetBoneObject( 2 )?.Flags |= GameObjectFlags.ProceduralBone;
renderer.GetBoneObject( 3 )?.WorldTransform = new Transform( d, line_rot ); ;
renderer.GetBoneObject( 3 )?.Flags |= GameObjectFlags.ProceduralBone;
}
}
private float GetEase( float delta, EaseType easeIn )
{
switch ( easeIn )
{
case EaseType.Linear: return delta;
case EaseType.EaseIn: return Easing.EaseIn( delta );
case EaseType.EaseOut: return Easing.EaseOut( delta );
case EaseType.EaseInOut: return Easing.EaseInOut( delta );
case EaseType.Bounce: return Easing.BounceOut( delta );
}
return delta;
}
}