Level/DoorComponent.cs
using Sandbox;
public class DoorComponent : BaseInteract, Component.IDamageable
{
[Property, Group( "Movement" )] public Angles MoveDir { get; set; }
[Property, Group( "Movement" )] public float Distance { get; set; } = 90.0f;
[Property, Group( "Movement" )] public float DoorSpeed { get; set; } = 3f;
[Property, Range( -120, 120 ), Group( "Movement" )] public float StartingRotation { get; set; } = 0.0f;
[Property, Group( "Sound" )] public SoundEvent OpenSound { get; set; }
[Property, Group( "Sound" )] public SoundEvent CloseSound { get; set; }
[Property, Group( "Sound" )] public SoundEvent FullyCloseSound { get; set; }
[Property, Group( "Sound" )] public SoundEvent LockedSound { get; set; }
[Property, Group( "Sound" )] public SoundEvent UnlockedSound { get; set; }
[Property] public bool Locked { get; set; } = false;
[Property] Model HandleModel { get; set; }
[Property, Group( "Blocked" )] public bool IsBlocked { get; set; } = false;
[Property, Group( "Blocked" )] public List<GameObject> Blockers { get; set; } = new List<GameObject>();
[Property, Range( 0.25f, 0.5f ), Group( "Physics" )] public float PeekFraction { get; set; } = 0.33f;
[Property, Range( 10.0f, 50.0f ), Group( "Physics" )] public float Weight { get; set; } = 25.0f;
[Property] public Action OnUnlocked { get; set; }
private Rotation startRotation;
private Rotation targetRotation;
private float swingVelocity = 0f;
private float fracOpen; // 0..1
private enum States
{
/// <summary>
/// Can't move door, not rotating.
/// </summary>
Closed,
/// <summary>
/// Can't move door, but rotating to fixed ~10deg position.
/// </summary>
Opening,
/// <summary>
/// Can move door, door will rotate.
/// </summary>
Open,
/// <summary>
/// Player pressed E on an open door, so we'll force it to 100% open
/// </summary>
ForcedOpen,
/// <summary>
/// Can't move door, but rotating to close point
/// </summary>
Closing,
};
private float openingDirection = 1.0f;
private States state;
/// <summary>
/// Is this door open or in the process of opening?
/// </summary>
public bool IsOpen => MathF.Abs( fracOpen ) > 0.05f;
private GameObject HandleObject { get; set; }
private Vector3 HandlePos { get; set; } = new Vector3( 40, 0, 48 );
private Model DoorModel => GameObject.Components.Get<ModelRenderer>( FindMode.InSelf ).Model;
[Property] private Opium.InventoryItemResource ItemResource { get; set; }
public bool HasKey()
{
// TODO: Make this derive from the interaction instead of this hack
var player = Scene.GetAllComponents<Opium.PlayerController>().First();
var inventory = player.Inventory;
if ( inventory.HasItem( ItemResource ) )
{
return true;
}
return false;
}
public override string GetUseIcon()
{
if ( IsBlocked )
return "ui/interactions/blocked_door.png";
if ( HasKey() && Locked )
return "ui/interactions/unlocked_door.png";
if ( Locked )
return "ui/interactions/locked_door.png";
return null;
}
protected override void DrawGizmos()
{
base.DrawGizmos();
if ( !Gizmo.IsSelected ) return;
var gizmodoorRotation = MathF.Sin( Time.Now * DoorSpeed ).Remap( -1, 1, -MoveDir.Normal.AsVector3().Length, Distance ).LerpTo( -MoveDir.Normal.AsVector3().Length, Time.Delta );
Gizmo.Draw.Color = Color.Red;
Gizmo.Transform = new Transform( Transform.Position, Transform.Rotation * Rotation.FromAxis( -MoveDir.Normal.AsVector3(), gizmodoorRotation ), Transform.Scale );
Gizmo.Draw.Color = Color.Blue.WithAlpha( 1f );
Gizmo.Draw.LineBBox( DoorModel.Bounds );
Gizmo.Draw.Color = Color.Blue.WithAlpha( 0.5f );
Gizmo.Draw.SolidBox( DoorModel.Bounds );
if ( StartingRotation != 0 )
{
Gizmo.Transform = new Transform( Transform.Position, Transform.Rotation * Rotation.FromAxis( -MoveDir.Normal.AsVector3(), StartingRotation ), Transform.Scale );
Gizmo.Draw.Color = Color.Green.WithAlpha( 1f );
Gizmo.Draw.LineBBox( DoorModel.Bounds );
Gizmo.Draw.Color = Color.Green.WithAlpha( 0.25f );
Gizmo.Draw.SolidBox( DoorModel.Bounds );
}
if ( Game.IsPlaying )
{
Gizmo.Draw.Color = Color.White;
Gizmo.Transform = global::Transform.Zero;
Gizmo.Draw.Text( $"State: {state}", Transform.World ); ;
}
}
protected override void OnStart()
{
base.OnStart();
startRotation = Transform.LocalRotation;
if ( HandleModel != null )
{
HandleObject = new GameObject( true, "handle" );
HandleObject.Parent = GameObject;
HandleObject.Transform.Position = Transform.Position + Transform.Rotation * HandlePos;
HandleObject.Components.Create<ModelRenderer>().Model = HandleModel;
}
if ( StartingRotation != 0 )
{
Transform.Rotation = Transform.Rotation * Rotation.FromAxis( -MoveDir.Normal.AsVector3(), StartingRotation );
fracOpen = StartingRotation / Distance;
state = States.Open;
}
}
private void UpdatePhysics()
{
var renderer = Components.Get<ModelRenderer>();
var origin = renderer.Bounds.Center;
var nearbyObjects = Scene.FindInPhysics( BBox.FromPositionAndSize( origin, 10f ) )
.Where( x => x != GameObject );
foreach ( var go in nearbyObjects )
{
if ( go.Components.GetInAncestorsOrSelf<Opium.PlayerController>() is Opium.PlayerController player )
{
var openDirection = GetOpenDirection( go );
var f = player.WishVelocity.Length * openDirection * 0.0003f * Weight;
Rotate( f );
}
}
}
private float GetOpenDirection( GameObject player )
{
var doorToPlayer = player.Transform.Position - Transform.Position;
return doorToPlayer.Dot( Transform.Rotation.Forward ) > 0 ? -1.0f : 1.0f; ;
}
private void UpdateRotation()
{
swingVelocity = swingVelocity.LerpTo( 0.0f, Time.Delta );
RotateTo( fracOpen + swingVelocity * Time.Delta );
}
private void RotateTo( float target )
{
fracOpen = fracOpen.LerpTo( target, Time.Delta * DoorSpeed );
targetRotation = Rotation.Lerp( targetRotation, startRotation, Time.Delta * DoorSpeed * 0.1f );
Transform.Rotation = startRotation * Rotation.FromAxis( -MoveDir.Normal.AsVector3(), fracOpen * Distance );
fracOpen = fracOpen.Clamp( -1, 1 );
}
protected override void OnFixedUpdate()
{
if ( IsBlocked )
{
if ( Blockers.Count == 0 )
{
IsBlocked = false;
}
else
{
foreach ( var blocker in Blockers )
{
if ( blocker.IsValid() )
{
IsBlocked = true;
return;
}
else
{
IsBlocked = false;
}
}
}
}
switch ( state )
{
case States.Closed:
break;
case States.Open:
{
if ( MathF.Abs( fracOpen ) >= 1.0f ) break;
if ( !Locked )
{
UpdatePhysics();
UpdateRotation();
}
}
break;
case States.Opening:
RotateTo( openingDirection * PeekFraction );
if ( MathF.Abs( fracOpen ) > (PeekFraction - 0.05f) )
state = States.Open;
break;
case States.Closing:
RotateTo( openingDirection * 0f );
if ( MathF.Abs( fracOpen ).AlmostEqual( 0f, 0.01f ) )
state = States.Closed;
break;
case States.ForcedOpen:
RotateTo( openingDirection * 1.0f );
break;
}
}
private void Rotate( float target )
{
swingVelocity += target;
}
public override void OnUse( GameObject player )
{
ToggleDoor( player );
}
public override void OnUseFail( GameObject player )
{
Sound.Play( LockedSound, Transform.Position );
}
public override bool CanUse( GameObject player )
{
return CanToggleDoor();
}
public bool CanToggleDoor()
{
if ( Locked )
{
return HasKey();
}
return (state == States.Open || state == States.ForcedOpen || state == States.Closed) && !IsBlocked;
}
public bool ToggleDoor( GameObject player )
{
if ( state == States.ForcedOpen || MathF.Abs( fracOpen ) > 0.8f )
{
swingVelocity = 0;
state = States.Closing;
return true;
}
if ( state == States.Open )
{
swingVelocity = 0;
state = States.ForcedOpen;
return true;
}
else if ( state == States.Closed )
{
if ( Locked )
{
if ( HasKey() )
{
Locked = false;
Sound.Play( UnlockedSound, Transform.Position );
var inventory = player.Components.Get<PlayerInventory>( FindMode.EverythingInSelfAndDescendants );
var data = inventory.FindItem( ItemResource );
data.Container?.RemoveItem( data.Index );
OnUnlocked?.Invoke();
return false;
}
else
{
return false;
}
}
OnPlayerOpen( player );
return true;
}
return false;
}
private void OnPlayerOpen( GameObject player )
{
if ( OpenSound != null && IsOpen )
{
Sound.Play( CloseSound, Transform.Position );
}
if ( OpenSound != null && !IsOpen )
{
Sound.Play( OpenSound, Transform.Position );
}
state = States.Opening;
openingDirection = GetOpenDirection( player );
}
public void OnDamage( in DamageInfo damage )
{
if ( IsBlocked ) return;
if ( Locked ) return;
if ( damage is Opium.DamageInfo dmg && dmg.HasTag( "open_door" ) )
{
if ( !IsOpen ) ToggleDoor( damage.Attacker );
}
}
}