mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-08-01 16:28:36 -04:00
* Include "Doo" in all names so there's no overlap with other variables/methods that a component might have (like IsRunning or Stop) * Make sure previous functions still work, obsoleted, telling user to use new methods. (These are implemented as extension methods so that member variables cannot conflict with the same name)
373 lines
9.0 KiB
C#
373 lines
9.0 KiB
C#
namespace Sandbox.Mapping;
|
|
|
|
[EditorHandle( Icon = "touch_app" )]
|
|
[Category( "Mapping" ), Alias( "Button" ), Icon( "touch_app" )]
|
|
public sealed class Button : Component, Component.IPressable
|
|
{
|
|
/// <summary>
|
|
/// Sound to play when the button is pressed.
|
|
/// </summary>
|
|
[Property, Group( "Sound" )] public SoundEvent OnSound { get; set; }
|
|
|
|
/// <summary>
|
|
/// Sound to play when the button is released.
|
|
/// </summary>
|
|
[Property, Group( "Sound" )] public SoundEvent OffSound { get; set; }
|
|
|
|
/// <summary>
|
|
/// Called when the button is pressed. Receives the GameObject that pressed it.
|
|
/// </summary>
|
|
[Property, Group( "Events" ), Doo.ArgumentHint<GameObject>( "user", Help = "The person using the button." )]
|
|
public Doo OnPressed { get; set; }
|
|
|
|
/// <summary>
|
|
/// Called when the button is released. Receives the GameObject that released it.
|
|
/// </summary>
|
|
[Property, Group( "Events" ), Doo.ArgumentHint<GameObject>( "user", Help = "The person has stopped using the button." )]
|
|
public Doo OnReleased { get; set; }
|
|
|
|
/// <summary>
|
|
/// Called when the button turns on. Receives the GameObject that activated it.
|
|
/// </summary>
|
|
[Property, Group( "Events" ), Doo.ArgumentHint<GameObject>( "user", Help = "The person who activated the button." )]
|
|
public Doo OnTurnedOn { get; set; }
|
|
|
|
/// <summary>
|
|
/// Called when the button turns off.
|
|
/// </summary>
|
|
[Property, Group( "Events" )]
|
|
public Doo OnTurnedOff { get; set; }
|
|
|
|
/// <summary>
|
|
/// The button's behavior mode.
|
|
/// </summary>
|
|
public enum ButtonMode
|
|
{
|
|
/// <summary>
|
|
/// Normal toggle button - click to turn on, click again to turn off.
|
|
/// </summary>
|
|
Toggle,
|
|
/// <summary>
|
|
/// Button is only on while being held down.
|
|
/// </summary>
|
|
Continuous,
|
|
/// <summary>
|
|
/// Button briefly turns on then immediately off when pressed.
|
|
/// </summary>
|
|
Immediate
|
|
}
|
|
|
|
[Property] public ButtonMode Mode { get; set; } = ButtonMode.Toggle;
|
|
|
|
[Property, ShowIf( "Mode", ButtonMode.Toggle )] public bool AutoReset { get; set; } = true;
|
|
[Property, ShowIf( "AutoReset", true )] public float ResetTime { get; set; } = 1.0f;
|
|
|
|
[Property, Group( "Movement" ), Order( 0 )] public bool Move { get; set; }
|
|
[Property, Group( "Movement" ), ShowIf( nameof( Move ), true )] public GameObject MoveTarget { get; set; }
|
|
[Property, Group( "Movement" ), ShowIf( nameof( Move ), true )] public Vector3 MoveDelta { get; set; }
|
|
|
|
/// <summary>
|
|
/// Animation curve to use, X is the time between 0-1 and Y is how much the button is pressed from 0-1.
|
|
/// </summary>
|
|
[Property, Group( "Movement" ), ShowIf( nameof( Move ), true )] public Curve AnimationCurve { get; set; } = new Curve( new Curve.Frame( 0f, 0f ), new Curve.Frame( 1f, 1.0f ) );
|
|
|
|
/// <summary>
|
|
/// How long in seconds should it take to animate this button.
|
|
/// </summary>
|
|
[Property, Group( "Movement" ), ShowIf( nameof( Move ), true )] public float AnimationTime { get; set; } = 0.5f;
|
|
|
|
Vector3 _initialPosition;
|
|
Transform _startTransform;
|
|
bool _isBeingPressed;
|
|
bool _shouldTurnOffNextFrame;
|
|
GameObject _lastPresser;
|
|
|
|
[Sync] private TimeSince LastUse { get; set; }
|
|
[Sync] private bool _isOn { get; set; }
|
|
|
|
/// <summary>
|
|
/// True if the button is currently on
|
|
/// </summary>
|
|
public bool IsOn
|
|
{
|
|
get => _isOn;
|
|
private set
|
|
{
|
|
if ( _isOn == value )
|
|
return;
|
|
|
|
_isOn = value;
|
|
OnButtonStateChanged( value );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// True if the button is currently animating
|
|
/// </summary>
|
|
public bool IsAnimating { get; private set; }
|
|
|
|
void OnButtonStateChanged( bool isOn )
|
|
{
|
|
if ( isOn )
|
|
{
|
|
RunDoo( OnTurnedOn, c => c.SetArgument( "user", _lastPresser ) );
|
|
}
|
|
else
|
|
{
|
|
RunDoo( OnTurnedOff );
|
|
}
|
|
}
|
|
|
|
protected override void DrawGizmos()
|
|
{
|
|
if ( !Gizmo.IsSelected )
|
|
return;
|
|
|
|
if ( !Move )
|
|
return;
|
|
|
|
Gizmo.Transform = WorldTransform;
|
|
|
|
var bbox = GameObject.GetLocalBounds();
|
|
bbox += MoveDelta * MathF.Sin( RealTime.Now * 2.0f ).Remap( -1, 1 );
|
|
|
|
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 );
|
|
}
|
|
|
|
protected override void OnStart()
|
|
{
|
|
var moveTarget = MoveTarget.IsValid() ? MoveTarget : GameObject;
|
|
_startTransform = moveTarget.LocalTransform;
|
|
_initialPosition = _startTransform.Position;
|
|
}
|
|
|
|
bool IPressable.CanPress( IPressable.Event e )
|
|
{
|
|
return !IsAnimating;
|
|
}
|
|
|
|
bool IPressable.Pressing( IPressable.Event e )
|
|
{
|
|
_isBeingPressed = true;
|
|
return Mode == ButtonMode.Continuous;
|
|
}
|
|
|
|
bool IPressable.Press( IPressable.Event e )
|
|
{
|
|
Press( e.Source.GameObject );
|
|
return true;
|
|
}
|
|
|
|
void IPressable.Release( IPressable.Event e )
|
|
{
|
|
_isBeingPressed = false;
|
|
Release( e.Source.GameObject );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the button on. Does nothing if already on or animating.
|
|
/// </summary>
|
|
[Rpc.Host]
|
|
public void TurnOn( GameObject presser = null )
|
|
{
|
|
// Don't do anything if already on or animating
|
|
if ( IsOn || IsAnimating )
|
|
return;
|
|
|
|
_lastPresser = presser;
|
|
LastUse = 0;
|
|
IsAnimating = true;
|
|
IsOn = true;
|
|
|
|
if ( OnSound is not null )
|
|
PlaySound( OnSound );
|
|
|
|
if ( Mode == ButtonMode.Immediate )
|
|
{
|
|
_shouldTurnOffNextFrame = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Turns the button off. Does nothing if already off or animating.
|
|
/// </summary>
|
|
[Rpc.Host]
|
|
public void TurnOff()
|
|
{
|
|
// Don't do anything if already off or animating
|
|
if ( !IsOn || IsAnimating )
|
|
return;
|
|
|
|
LastUse = 0;
|
|
IsAnimating = true;
|
|
IsOn = false;
|
|
|
|
if ( OffSound is not null )
|
|
PlaySound( OffSound );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggles the button between on and off states.
|
|
/// </summary>
|
|
[Rpc.Host]
|
|
public void Toggle( GameObject presser )
|
|
{
|
|
if ( !IsOn )
|
|
{
|
|
TurnOn( presser );
|
|
}
|
|
else
|
|
{
|
|
TurnOff();
|
|
}
|
|
}
|
|
|
|
[Rpc.Host]
|
|
private void Press( GameObject presser )
|
|
{
|
|
if ( IsAnimating )
|
|
return;
|
|
|
|
_lastPresser = presser;
|
|
RunDoo( OnPressed, c => c.SetArgument( "user", _lastPresser ) );
|
|
|
|
switch ( Mode )
|
|
{
|
|
case ButtonMode.Toggle:
|
|
if ( IsOn && !AutoReset )
|
|
{
|
|
TurnOff();
|
|
}
|
|
else
|
|
{
|
|
TurnOn( presser );
|
|
}
|
|
break;
|
|
|
|
case ButtonMode.Continuous:
|
|
TurnOn( presser );
|
|
break;
|
|
|
|
case ButtonMode.Immediate:
|
|
TurnOn( presser );
|
|
break;
|
|
}
|
|
}
|
|
|
|
[Rpc.Host]
|
|
private void Release( GameObject presser )
|
|
{
|
|
RunDoo( OnReleased, c => c.SetArgument( "user", _lastPresser ) );
|
|
|
|
// For continuous mode, turn off when released
|
|
if ( Mode == ButtonMode.Continuous && IsOn )
|
|
{
|
|
TurnOff();
|
|
}
|
|
}
|
|
|
|
[Rpc.Broadcast]
|
|
private void PlaySound( SoundEvent sound )
|
|
{
|
|
GameObject.PlaySound( sound );
|
|
}
|
|
|
|
protected override void OnFixedUpdate()
|
|
{
|
|
//
|
|
// Okay, this part could be way better
|
|
//
|
|
if ( Mode == ButtonMode.Immediate && _shouldTurnOffNextFrame && IsOn && !IsAnimating )
|
|
{
|
|
_shouldTurnOffNextFrame = false;
|
|
TurnOff();
|
|
return;
|
|
}
|
|
|
|
if ( Mode == ButtonMode.Toggle && !IsAnimating && IsOn && AutoReset && ResetTime >= 0.0f && LastUse >= ResetTime )
|
|
{
|
|
TurnOff();
|
|
return;
|
|
}
|
|
|
|
if ( Mode == ButtonMode.Continuous && IsOn && !IsAnimating && !_isBeingPressed )
|
|
{
|
|
TurnOff();
|
|
return;
|
|
}
|
|
|
|
// Continuous: Reset the pressing flag each frame - it will be set again if still being pressed
|
|
if ( Mode == ButtonMode.Continuous )
|
|
{
|
|
_isBeingPressed = false;
|
|
}
|
|
|
|
// Don't do anything if we're not animating
|
|
if ( !IsAnimating )
|
|
return;
|
|
|
|
// Normalize the last use time to the amount of time to animate
|
|
var time = LastUse.Relative.Remap( 0.0f, AnimationTime, 0.0f, 1.0f );
|
|
|
|
// Evaluate our animation curve
|
|
var curve = AnimationCurve.Evaluate( time );
|
|
|
|
// Animate backwards if we're turning off (IsOn is false when turning off)
|
|
if ( !IsOn ) curve = 1.0f - curve;
|
|
|
|
if ( Move )
|
|
{
|
|
var target = MoveTarget.IsValid() ? MoveTarget : GameObject;
|
|
var targetPosition = _initialPosition + target.LocalTransform.Rotation * (MoveDelta * curve);
|
|
target.LocalTransform = _startTransform.WithPosition( targetPosition );
|
|
}
|
|
|
|
// If we're done, finalize the animation
|
|
if ( time < 1f ) return;
|
|
|
|
IsAnimating = false;
|
|
}
|
|
|
|
[Property, Feature( "Tooltip" )]
|
|
public string TooltipTitle { get; set; } = "Press";
|
|
|
|
[Property, Feature( "Tooltip" ), IconName]
|
|
public string TooltipIcon { get; set; } = "touch_app";
|
|
|
|
[Property, Feature( "Tooltip" )]
|
|
public string TooltipDescription { get; set; } = "";
|
|
|
|
[Header( "Off State" )]
|
|
[ShowIf( "Mode", ButtonMode.Toggle )]
|
|
[Property, Feature( "Tooltip" )]
|
|
public string TooltipTitleOff { get; set; } = "Press";
|
|
|
|
[ShowIf( "Mode", ButtonMode.Toggle )]
|
|
[Property, Feature( "Tooltip" ), IconName]
|
|
public string TooltipIconOff { get; set; } = "touch_app";
|
|
|
|
[ShowIf( "Mode", ButtonMode.Toggle )]
|
|
[Property, Feature( "Tooltip" )]
|
|
public string TooltipDescriptionOff { get; set; } = "";
|
|
|
|
IPressable.Tooltip? IPressable.GetTooltip( IPressable.Event e )
|
|
{
|
|
if ( string.IsNullOrWhiteSpace( TooltipTitle ) && string.IsNullOrWhiteSpace( TooltipIcon ) )
|
|
return default;
|
|
|
|
if ( Mode == ButtonMode.Toggle && IsOn )
|
|
{
|
|
return new IPressable.Tooltip( TooltipTitleOff, TooltipIconOff, TooltipDescriptionOff );
|
|
}
|
|
|
|
return new IPressable.Tooltip( TooltipTitle, TooltipIcon, TooltipDescription );
|
|
}
|
|
}
|