VaultController.cs
using Sandbox;
using System;
namespace PaintballBaddies;
/// <summary>Short, swept kinematic traversal of a prevalidated low obstacle.</summary>
public sealed class VaultController : Component
{
public bool IsVaulting { get; private set; }
internal void ApplyNetwork(NetworkPawn state){IsVaulting=state.Vaulting;Progress=state.VaultProgress;Facing=state.VaultFacing;}
private bool routeAvailable;
private float feedbackTime;
public string BlockReason { get; private set; } = "";
public bool Available => Enabled && routeAvailable && !IsVaulting && player.IsValid() && !player.IsAirborne;
public string Hint => IsVaulting ? "VAULTING" : Available ? $"{PaintballControls.Display(Scene,"vault")} VAULT" : feedbackTime>0 ? BlockReason : "";
public float Progress { get; private set; }
public int Completed { get; private set; }
public string Outcome { get; private set; } = "";
public Vector3 VisualBasePosition { get; private set; }
public Rotation Facing { get; private set; }
private PlayerController player;
private VaultPlanner.Plan plan;
private bool inputBefore, motionBefore;
private float probeTime;
public const float Duration = .8f;
protected override void OnStart() => player = Components.Get<PlayerController>();
public bool TryBegin()
{
// Revalidate on the press; attached cover supplies its inward direction
// even when the camera is looking along the wall.
var cover = Components.Get<CoverController>();
if ( !Enabled || IsVaulting || !player.IsValid() || Components.Get<CloseCombat>()?.Incapacitated==true ) return false;
if(player.IsAirborne){BlockReason="Land before vaulting";feedbackTime=2;return false;}
if(!VaultPlanner.TryPlan(player,out plan,out var reason)){BlockReason=reason;Outcome=reason;feedbackTime=2;return false;}
feedbackTime=0;BlockReason="";
cover?.Leave();
inputBefore = player.UseInputControls; motionBefore = player.Body.MotionEnabled;
VisualBasePosition = player.Renderer.LocalPosition;
Facing = Rotation.LookAt( (plan.Landing - plan.Start).WithZ( 0 ) );
Progress = 0; IsVaulting = true; routeAvailable = false; Outcome = "Traversing";
Components.Get<PaintballMarker>()?.CancelReload();
player.UseInputControls = false; player.WishVelocity = Vector3.Zero; player.Body.Velocity = Vector3.Zero;
player.Body.MotionEnabled = false;
return true;
}
public void Cancel()
{
if(IsProxy)return;
routeAvailable = false;
if ( !IsVaulting ) return;
IsVaulting = false; Outcome = "Interrupted";
if ( !player.IsValid() ) return;
player.UseInputControls = inputBefore;
player.WishVelocity = Vector3.Zero;
if ( player.Body.IsValid() )
{
player.Body.MotionEnabled = motionBefore;
player.Body.Velocity = Vector3.Zero;
}
if ( player.Renderer.IsValid() ) player.Renderer.LocalPosition = VisualBasePosition;
}
protected override void OnDisabled() => Cancel();
protected override void OnDestroy() => Cancel();
protected override void OnUpdate()
{
if(IsProxy)return;
feedbackTime=MathF.Max(0,feedbackTime-Time.Delta);
if ( IsVaulting || player is null ) return;
var cover = Components.Get<CoverController>();
probeTime -= Time.Delta;
if ( probeTime <= 0 )
{
routeAvailable = VaultPlanner.TryPlan( player, out _, out var reason );
if(feedbackTime<=0)BlockReason=routeAvailable ? "" : reason;
probeTime = .1f;
}
if ( Components.Get<PaintballMarker>()?.AcceptInput == true && Input.Pressed( "vault" ) ) TryBegin();
}
private static float Smooth( float value ) { value = value.Clamp( 0, 1 ); return value * value * (3 - 2 * value); }
protected override void OnFixedUpdate()
{
if(IsProxy)return;
if ( !IsVaulting ) return;
Progress = (Progress + Time.Delta / Duration).Clamp( 0, 1 );
var destination = Progress < .2f ? Vector3.Lerp( plan.Start, plan.AboveStart, Smooth( Progress / .2f ) )
: Progress < .65f ? Vector3.Lerp( plan.AboveStart, plan.AboveLanding, Smooth( (Progress - .2f) / .45f ) )
: Vector3.Lerp( plan.AboveLanding, plan.Landing, MathF.Pow( ((Progress - .65f) / .35f).Clamp( 0, 1 ), 2 ) );
var body = new Capsule(Vector3.Up*17,Vector3.Up*(player.BodyHeight-16),16);
var sweep = Scene.Trace.Capsule( body, WorldPosition, destination ).IgnoreGameObjectHierarchy( GameObject ).WithoutTags( "paintball_debris" ).Run();
if ( sweep.Hit ) { Cancel(); Outcome = "Route became blocked"; return; }
WorldPosition = destination;
if ( Progress < 1 ) return;
Cancel(); Completed++; Outcome = "Landed";
}
}