Pawn/SkateController.cs
using Skateboard.Entities;
using Skateboard.Physics;
using Skateboard.Tricks;
using Skateboard.Utils;
using System;
namespace Skateboard.Player;
[Title( "Pro Skater physics and movement controller" )]
public sealed class SkateController : Component
{
private struct GrindCandidate
{
public Vector3 Start;
public Vector3 End;
public MathLD.NearestPoint Nearest;
}
public static float StoppedVelocity { get; set; } = 0.01f;
[Property] public SkatePawn Pawn { get; set; }
[Property] public SkateInput Input { get; set; }
[Property] public TrickScoreHolder TrickScores { get; set; }
[Property] public SkateAnimator Animator { get; set; }
[Property] public float JumpForce { get; set; } = 300f;
[Property] public float JumpMaxSpeed { get; set; } = 600f;
[Property] public float Gravity { get; set; } = 900f;
[Property] public float Acceleration { get; set; } = 400f;
[Property] public float TopSpeed { get; set; } = 1300f;
[Property] public float AirTopSpeed { get; set; } = 2000f;
[Property] public float BrakeForce { get; set; } = 600f;
[Property] public float TractionForce { get; set; } = 0f;
[Property] public float SteerSpeed { get; set; } = 150f;
[Property] public float BrakeSteerMultiplier { get; set; } = 1.5f;
[Property] public float PushMaxSpeed { get; set; } = 450f;
[Property] public float CrouchAcceleration { get; set; } = 600f;
[Property] public float CrouchMaxSpeed { get; set; } = 700f;
[Property] public float SmoothingSpeed { get; set; } = 10f;
[Property] public float MaxStandableAngle { get; set; } = 70f;
[Property] public float MinFallableAngle { get; set; } = 70f;
[Property] public float MaxSlope { get; set; } = 50f;
[Property] public float BodyGirth { get; set; } = 8f;
[Property] public float BodyHeight { get; set; } = 50f;
[Property] public float EyeHeight { get; set; } = 64f;
[Property] public float AirSpinSpeed { get; set; } = 325f;
[Property] public float AirPitchSpeed { get; set; } = 50f;
[Property] public float SidewaysBailSpeed { get; set; } = 600f;
[Property] public float LandBailMaxAngle { get; set; } = 60f;
[Property] public float LandSpeedMultiplier { get; set; } = 0.98f;
[Property] public float HitForce { get; set; } = 200f;
[Property] public float GrindCooldown { get; set; } = 0.2f;
[Property] public float GrindDeacceleration { get; set; } = 10f;
[Property] public float GrindSpeedBoost { get; set; } = 100f;
[Property] public float MinimumGrindTime { get; set; } = 0.3f;
[Property] public float GrindScoreInterval { get; set; } = 0.002f;
[Property] public float VertMinAngle { get; set; } = 45f;
[Property] public float AirOutSpeed { get; set; } = 2f;
[Property] public float CoyoteTime { get; set; } = 0.4f;
[Property] public float JumpStrengthMinimum { get; set; } = 0.75f;
[Property] public float JumpStrengthSpeed { get; set; } = 0.3f;
[Sync] public Vector3 Velocity { get; private set; }
[Sync] public Angles AngularVelocity { get; private set; }
[Sync] public Vector3 GroundNormal { get; private set; } = Vector3.Up;
[Sync] public bool OnGrind { get; private set; }
[Sync] public Vector3 GrindNormal { get; private set; }
[Sync] public Rotation GrindRotation { get; private set; }
[Sync] public Vector3 GrindStart { get; private set; }
[Sync] public Vector3 GrindEnd { get; private set; }
[Sync] public bool OnVert { get; private set; }
[Sync] public Vector3 VertNormal { get; private set; } = Vector3.Zero;
[Sync] public bool HasVertBelow { get; private set; }
[Sync] public bool AiringOut { get; private set; }
[Sync] public float NudgeAmount { get; private set; }
[Sync] public float SpunAmount { get; private set; }
[Sync] public bool Pushing { get; private set; }
[Sync] public bool JumpReady { get; private set; }
[Sync] public bool IsGrounded { get; private set; }
private float _currentCoyoteTime;
private bool _jumped;
private float _currentJumpStrength;
private bool GroundVert;
private Rotation LastGroundRotation = Rotation.Identity;
private GameObject GroundObject;
private Collider GroundCollider;
private Vector3 GroundVelocity;
private bool SnapRotation;
private float CurrentGrindCooldown;
private float CurrentGrindTime;
private float CurrentGrindScoreInterval;
private bool HitGrindTrick;
private TrickScoreEntry _currentGrindScoreEntry;
private TrickScoreEntry GrindTrick;
private float wallCollisionSphereRadius = 6f;
private float wallCollisionSphereHeight = 15f;
private float wallCollisionSphereGroundDistance = 5f;
private float wallCollisionSphereForce = 10f;
private float upsideDownDotThreshold = 0.1f;
private float upsideDownTraceOffset = 70f;
private float rotationTraceOffset = 2f;
protected override void OnStart()
{
if ( skate_sim_mode )
SetSimStats();
}
protected override void OnFixedUpdate()
{
Pawn ??= Components.Get<SkatePawn>();
Input ??= Components.Get<SkateInput>();
TrickScores ??= Components.Get<TrickScoreHolder>();
Animator ??= Components.Get<SkateAnimator>();
if ( Pawn is null || Input is null || TrickScores is null )
return;
if ( Networking.IsActive && !GameObject.Network.IsOwner && Pawn.OwningConnectionId != Connection.Local.Id )
return;
if ( Pawn.Bailed )
return;
Simulate();
}
[ConVar( ConVarFlags.Replicated )] public static bool skate_sim_mode { get; set; } = false;
private Vector3 Position
{
get => WorldPosition;
set => WorldPosition = value;
}
private Rotation Rotation
{
get => WorldRotation;
set => WorldRotation = value;
}
private Vector3 GetWishDir( Rotation facing )
{
var move = Input.Move;
var wish = (facing.Forward * move.y) + (facing.Right * move.x);
wish = wish.WithZ( 0f );
return wish.Length > 0.001f ? wish.Normal : Vector3.Zero;
}
private const float DigitalDeadzone = 0.75f;
private float DigitalForwardInput => Math.Abs( Input.Move.x ) <= DigitalDeadzone ? 0f : Math.Sign( Input.Move.x );
private float DigitalLeftInput => Math.Abs( Input.Move.y ) <= DigitalDeadzone ? 0f : Math.Sign( Input.Move.y );
private bool ForwardDown => DigitalForwardInput > 0f;
private bool BackDown => DigitalForwardInput < 0f;
private void SimulateGrind( SkatePawn pawn, ref Rotation realRot )
{
if ( CurrentGrindCooldown > 0f )
CurrentGrindCooldown -= Time.Delta;
if ( OnGrind )
{
CurrentGrindTime += Time.Delta;
if ( CurrentGrindTime >= MinimumGrindTime )
{
if ( _currentGrindScoreEntry != null )
{
CurrentGrindScoreInterval += Time.Delta;
if ( CurrentGrindScoreInterval >= GrindScoreInterval )
{
_currentGrindScoreEntry.Score += (int)Math.Floor( CurrentGrindScoreInterval / GrindScoreInterval );
CurrentGrindScoreInterval = 0f;
TrickScores.Refresh();
}
}
else
{
CurrentGrindScoreInterval = 0f;
}
}
if ( CurrentGrindTime >= MinimumGrindTime && !HitGrindTrick )
{
HitGrindTrick = true;
_currentGrindScoreEntry = new TrickScoreEntry( "50-50", 100 );
TrickScores.Add( _currentGrindScoreEntry );
}
Velocity -= GrindDeacceleration * Velocity.Normal * Time.Delta;
Velocity = Vector3.Dot( Velocity, GrindNormal ) * GrindNormal;
var closest = MathLD.NearestPointOnLine( Position, GrindStart, GrindEnd, 1f );
if ( closest.Outside && !TryStartGrind( ref realRot, connect: true ) )
{
StopGrind( pawn, manual: false );
_currentCoyoteTime = CoyoteTime;
}
Position = closest.Point;
if ( Velocity.Length <= 1f )
{
StopGrind( pawn, manual: false );
_currentCoyoteTime = CoyoteTime;
}
SpunAmount = 0f;
}
else
{
CurrentGrindTime = 0f;
HitGrindTrick = false;
}
if ( Input.GrindHeld && !OnGrind )
TryStartGrind( ref realRot );
}
private void StopGrind( SkatePawn pawn, bool manual )
{
if ( !OnGrind )
return;
OnGrind = false;
CurrentGrindCooldown = GrindCooldown;
if ( _currentGrindScoreEntry == null && !manual )
TrickScores.Add( new TrickScoreEntry( "Kissed the Rail", 50 ) );
_currentGrindScoreEntry = null;
}
private bool TryStartGrind( ref Rotation realRot, bool connect = false )
{
if ( CurrentGrindCooldown > 0f && !connect )
return false;
float closeDist = (IsGrounded || OnGrind) ? 10f : 75f;
var candidates = new List<GrindCandidate>();
foreach ( var ent in Scene.GetAllComponents<GrindPathEntity>() )
{
ent.RefreshPathNodes();
if ( ent.PathNodes is null || ent.PathNodes.Count < 2 )
continue;
for ( int i = 0; i < ent.PathNodes.Count - 1; i++ )
{
var nodeA = ent.PathNodes[i];
var nodeB = ent.PathNodes[i + 1];
if ( !nodeA.IsValid() || !nodeB.IsValid() )
continue;
var a = nodeA.WorldPosition;
var b = nodeB.WorldPosition;
var closest = MathLD.NearestPointOnLine( Position, a, b );
var dist = Vector3.DistanceBetween( Position, closest.Point );
if ( !connect )
{
float candidateHeading = (0f - (Math.Abs( Vector3.Dot( Velocity.Normal, closest.Direction ) ) - 1f)) * 10f;
dist += candidateHeading;
}
if ( dist > closeDist )
continue;
if ( OnGrind && a == GrindStart && b == GrindEnd )
continue;
if ( !connect && Vector3.Dot( (closest.Point - Position).Normal, Velocity.Normal ) < 0f )
continue;
if ( connect && Math.Abs( Vector3.Dot( Velocity.Normal, closest.Direction ) ) < 0.5f )
continue;
candidates.Add( new GrindCandidate
{
Start = a,
End = b,
Nearest = closest
} );
}
}
if ( candidates.Count == 0 )
return false;
var best = candidates[0];
var bestScore = ScoreCandidate( best, connect );
for ( int i = 1; i < candidates.Count; i++ )
{
var s = ScoreCandidate( candidates[i], connect );
if ( s < bestScore )
{
bestScore = s;
best = candidates[i];
}
}
ClearGroundEntity( ref realRot );
Position = best.Nearest.Point;
GrindStart = best.Start;
GrindEnd = best.End;
GrindNormal = best.Nearest.Direction;
OnGrind = true;
OnVert = false;
StartGrind( ref realRot, connect );
return true;
float ScoreCandidate( GrindCandidate c, bool isConnect )
{
var dist = Vector3.DistanceBetween( Position, c.Nearest.Point );
if ( !isConnect )
{
float heading = (0f - (Math.Abs( Vector3.Dot( Velocity.Normal, c.Nearest.Direction ) ) - 1f)) * 10f;
dist += heading;
}
return dist;
}
}
private void StartGrind( ref Rotation realRot, bool connect )
{
float oldSpeed = Velocity.Length;
float minSpeed = 500f;
Velocity = Vector3.Dot( Velocity, GrindNormal ) * GrindNormal;
if ( !connect )
FinishSpinTrick();
else
Velocity = oldSpeed * Velocity.Normal;
var grindDir = Velocity.Normal;
if ( Velocity.Length <= 5f )
grindDir = (realRot.Forward * GrindNormal);
GrindRotation = Rotation.LookAt( grindDir, Vector3.Up );
realRot = GrindRotation;
if ( Velocity.Length < minSpeed && !connect )
Velocity = realRot.Forward * minSpeed;
if ( !connect )
{
Velocity += realRot.Forward * GrindSpeedBoost;
Rotation = GrindRotation;
SnapRotation = true;
}
AngularVelocity = Angles.Zero;
}
private void CalculateSpinTrick()
{
if ( !IsGrounded )
SpunAmount += AngularVelocity.yaw * Time.Delta;
}
private void FinishSpinTrick()
{
double finalAmount = Math.Ceiling( Math.Abs( SpunAmount ) / 180f ) * 180.0;
if ( Math.Abs( SpunAmount ) % 180f <= 90f )
finalAmount -= 180.0;
if ( finalAmount <= 0.0 )
return;
string side = (SpunAmount < 0f) ? "FS" : "BS";
string name = $"{side} {finalAmount} Ollie";
double spins = finalAmount / 180.0 - 1.0;
var trick = new TrickScoreEntry( name, (int)(100.0 + spins * 50.0) );
TrickScores.Add( trick );
}
public void UpdateGroundEntity( SceneTraceResult tr )
{
FinishSpinTrick();
SpunAmount = 0f;
OnVert = false;
GroundNormal = tr.Normal;
GroundObject = tr.GameObject;
GroundCollider = tr.Collider;
IsGrounded = tr.Hit;
UpdateGroundVelocity();
}
public void ClearGroundEntity( ref Rotation realRot )
{
if ( !IsGrounded )
{
GroundObject = null;
GroundCollider = null;
GroundNormal = Vector3.Up;
IsGrounded = false;
return;
}
NudgeAmount = 0f;
if ( GroundVert )
{
GroundVert = false;
OnVert = true;
VertNormal = new Vector3( GroundNormal.x, GroundNormal.y, 0f ).Normal;
Pawn.VertNormal = VertNormal;
Velocity -= Velocity.ProjectOnNormal( VertNormal );
Position += VertNormal * 4f;
realRot = MathLD.FromToRotation( Vector3.Up * realRot, VertNormal ) * realRot;
}
GroundObject = null;
GroundCollider = null;
GroundNormal = Vector3.Up;
IsGrounded = false;
}
private void UpdateGroundVelocity()
{
if ( GroundCollider.IsValid() )
{
GroundVelocity = GroundCollider.GetVelocityAtPoint( WorldPosition );
return;
}
GroundVelocity = Vector3.Zero;
}
private bool CanJump()
{
if ( IsGrounded ) return true;
if ( _currentCoyoteTime > 0f && !_jumped ) return true;
if ( OnGrind ) return true;
return false;
}
private void AirOut( SkatePawn pawn, bool force = false )
{
bool shouldAirOut = !IsGrounded;
if ( OnVert && Velocity.Dot( Vector3.Up ) <= 0f ) shouldAirOut = false;
if ( OnVert && !HasVertBelow ) shouldAirOut = true;
if ( !force && !shouldAirOut )
return;
if ( OnVert )
{
Velocity -= VertNormal * 25f;
Position += pawn.RealRotation.Up * BodyGirth;
}
AiringOut = true;
OnVert = false;
pawn.OnVert = false;
}
private bool IsValidVert( Vector3 normal ) => normal.Angle( Vector3.Up ) >= VertMinAngle;
private void SimulateVert( SkatePawn pawn, ref Rotation realRot )
{
HasVertBelow = false;
const float traceLen = 100000f;
float inside = 0f;
const float insideOffset = 2f;
var from = Position + realRot.Up * inside;
var to = from - Vector3.Up * traceLen;
var vertTrace = Scene.Trace.Ray( from, to ).WithAnyTags( "vert" );
var result = vertTrace.Run();
if ( result.GameObject != null && IsValidVert( result.Normal ) )
{
HasVertBelow = true;
var vertNormal = (result.Normal - result.Normal.ProjectOnNormal( Vector3.Up )).Normal;
var lastResult = result;
while ( true )
{
inside -= 0.5f;
from = Position + realRot.Up * inside;
to = from - Vector3.Up * traceLen;
var r2 = Scene.Trace.Ray( from, to ).WithAnyTags( "vert" ).Run();
var v2 = (r2.Normal - r2.Normal.ProjectOnNormal( Vector3.Up )).Normal;
if ( r2.GameObject != null && v2 == vertNormal )
{
vertNormal = v2;
lastResult = r2;
continue;
}
break;
}
result = lastResult;
Position = new Vector3( result.HitPosition.x, result.HitPosition.y, Position.z );
realRot = MathLD.FromToRotation( Vector3.Up * realRot, vertNormal ) * realRot;
Position += vertNormal * insideOffset;
Position += realRot.Up * NudgeAmount;
VertNormal = vertNormal;
pawn.VertNormal = vertNormal;
}
Velocity -= Velocity.ProjectOnNormal( VertNormal );
}
private void ClampTopSpeed()
{
float top = IsGrounded ? TopSpeed : AirTopSpeed;
if ( Velocity.Length > top )
Velocity = Velocity.Normal * top;
}
private void SetSimStats()
{
Gravity = 850f;
TopSpeed = 1000f;
JumpForce = 240f;
CoyoteTime = 0.1f;
SidewaysBailSpeed = 320f;
LandBailMaxAngle = 50f;
AirOutSpeed = 1f;
}
private void DoUpsideDownFall( ref Rotation realRot, ref bool isFalling )
{
isFalling = false;
if ( !IsGrounded )
return;
float angle = GroundNormal.Angle( Vector3.Up );
if ( angle < MinFallableAngle )
return;
float requiredSpeed = angle * 4f;
float vel = Vector3.Dot( realRot.Forward, Velocity );
float downRotationSpeed = angle * 0.025f;
float downForwardSpeed = angle * 2f;
var downAngle = Rotation.LookAt( Vector3.Down, GroundNormal );
var downRotation = MathLD.FromToRotation( Vector3.Up * downAngle, GroundNormal ) * downAngle;
if ( realRot.Forward.Dot( downRotation.Forward ) <= 0f )
downForwardSpeed = angle * 2f;
Velocity += downRotation.Forward * downForwardSpeed * Time.Delta;
if ( vel >= requiredSpeed )
return;
isFalling = true;
realRot = Rotation.Lerp( realRot, downRotation, downRotationSpeed * Time.Delta );
if ( angle > 90f )
{
requiredSpeed = angle * 4f;
Debug( requiredSpeed );
if ( vel < requiredSpeed )
{
Position += GroundNormal * 15f;
ClearGroundEntity( ref realRot );
Debug( "you fell off" );
}
}
}
private void Debug( object log )
{
if ( SkateGame.skate_debug )
Log.Info( log.ToString() );
}
private void Simulate()
{
var pawn = Pawn;
pawn.TurnRight = 0f - DigitalLeftInput;
pawn.OnVert = OnVert;
var realRot = pawn.RealRotation;
bool stopped = Velocity.Length <= StoppedVelocity;
SimulateGrind( pawn, ref realRot );
bool jump = false;
if ( Input.JumpHeld )
{
JumpReady = true;
}
else
{
if ( JumpReady && CanJump() )
jump = true;
else
_currentJumpStrength = JumpStrengthMinimum;
JumpReady = false;
}
if ( stopped )
{
if ( ForwardDown )
Pushing = true;
}
else if ( IsGrounded )
{
Pushing = !BackDown;
}
bool hardTurn = false;
bool braking = BackDown;
if ( braking )
Pushing = false;
if ( JumpReady && !braking )
Pushing = true;
if ( DigitalLeftInput != 0f && braking )
{
braking = false;
hardTurn = true;
}
bool falling = false;
DoUpsideDownFall( ref realRot, ref falling );
if ( falling )
braking = false;
pawn.Crouch = JumpReady;
pawn.EyeRotation = realRot;
realRot = realRot
.RotateAroundAxis( Vector3.Left, AngularVelocity.pitch * Time.Delta )
.RotateAroundAxis( Vector3.Left, AngularVelocity.roll * Time.Delta )
.RotateAroundAxis( Vector3.Up, AngularVelocity.yaw * Time.Delta );
Rotation = Rotation
.RotateAroundAxis( Vector3.Left, AngularVelocity.pitch * Time.Delta )
.RotateAroundAxis( Vector3.Left, AngularVelocity.roll * Time.Delta )
.RotateAroundAxis( Vector3.Up, AngularVelocity.yaw * Time.Delta );
if ( IsGrounded )
AiringOut = false;
if ( AiringOut && !IsGrounded )
{
var desired = MathLD.FromToRotation( Vector3.Up * realRot, Vector3.Up ) * realRot;
realRot = Rotation.Slerp( realRot, desired, AirOutSpeed * Time.Delta );
}
if ( Input.CrouchPressed )
AirOut( pawn );
HasVertBelow = false;
if ( OnVert )
SimulateVert( pawn, ref realRot );
if ( !IsGrounded )
{
if ( !OnGrind )
{
if ( !OnVert )
{
AngularVelocity = new Angles( DigitalForwardInput * AirPitchSpeed, DigitalLeftInput * AirSpinSpeed, 0f );
}
else
{
AngularVelocity = new Angles( 0f, DigitalLeftInput * AirSpinSpeed, 0f );
}
CalculateSpinTrick();
}
Velocity += Gravity * Vector3.Down * Time.Delta;
if ( OnGrind )
Velocity = Velocity.Dot( GrindNormal ) * GrindNormal;
}
else
{
AngularVelocity = new Angles( 0f, DigitalLeftInput * SteerSpeed * (hardTurn ? BrakeSteerMultiplier : 1f), 0f );
Velocity -= Velocity.ProjectOnNormal( GroundNormal );
var forwardOnly = Velocity.ProjectOnNormal( realRot.Forward );
var sideOnly = Velocity.ProjectOnNormal( realRot.Right );
float leftToTraction = sideOnly.Length;
float traction = TractionForce * Time.Delta;
if ( traction > leftToTraction ) traction = leftToTraction;
sideOnly -= traction * sideOnly.Normal;
Velocity = forwardOnly;
if ( Pushing )
{
float pushAccel = Acceleration;
float pushSpeed = PushMaxSpeed;
if ( JumpReady )
{
pushAccel = CrouchAcceleration;
pushSpeed = CrouchMaxSpeed;
}
if ( Velocity.Length < pushSpeed )
{
Velocity += Vector3.Forward * realRot * pushAccel * Time.Delta;
if ( Velocity.Length > pushSpeed )
Velocity = Velocity.Normal * pushSpeed;
}
}
else if ( braking )
{
float leftToStop = Velocity.Length;
if ( leftToStop > 0f )
{
float deaccel = BrakeForce * Time.Delta;
if ( deaccel > leftToStop ) deaccel = leftToStop;
Velocity -= deaccel * Velocity.Normal;
}
}
}
var helper = new SkateHelper( Scene.Trace, GameObject, Position, Velocity )
{
MaxStandableAngle = MaxStandableAngle
};
float collHeight = 15f;
float girth = BodyGirth;
if ( IsGrounded )
LastGroundRotation = realRot;
var mins = new Vector3( -girth, -girth, 0f );
var maxs = new Vector3( girth, girth, BodyHeight );
if ( !OnVert )
{
mins += LastGroundRotation.Up * collHeight;
maxs += LastGroundRotation.Up * collHeight;
float upsideDownAmount = Math.Min( 1f, Math.Abs( Vector3.Dot( LastGroundRotation.Up, Vector3.Up ) - 1f ) );
maxs -= BodyHeight * Vector3.Up * upsideDownAmount * 1.5f;
mins += BodyHeight * Vector3.Up * upsideDownAmount * 0.75f;
}
else
{
mins += Vector3.Up * collHeight;
maxs += Vector3.Up * collHeight;
mins += realRot.Up * BodyGirth;
maxs += realRot.Up * BodyGirth;
LastGroundRotation = Rotation.Identity;
}
helper.Trace = helper.Trace.Size( mins, maxs );
if ( helper.TryMove( Time.Delta, inAir: false ) > 0f )
{
if ( (helper.HitFloor || helper.Velocity.Length <= float.Epsilon) && !IsGrounded && !OnGrind )
{
if ( OnVert )
{
NudgeAmount += 4f;
}
else
{
var traceResults = new SceneTraceResult[4];
var testPosition = Position + Vector3.Up * 50f;
var pos = testPosition + realRot.Right * 10f;
var to = pos + Vector3.Down * 100f;
traceResults[0] = Scene.Trace.Ray( pos, to )
.WithAnyTags( "solid", "skateable" )
.WithoutTags( "unskateable" )
.Run();
pos = testPosition + realRot.Left * 10f;
to = pos + Vector3.Down * 100f;
traceResults[1] = Scene.Trace.Ray( pos, to )
.WithAnyTags( "solid", "skateable" )
.WithoutTags( "unskateable" )
.Run();
pos = testPosition + realRot.Forward * 10f;
to = pos + Vector3.Down * 100f;
traceResults[2] = Scene.Trace.Ray( pos, to )
.WithAnyTags( "solid", "skateable" )
.WithoutTags( "unskateable" )
.Run();
pos = testPosition + realRot.Backward * 10f;
to = pos + Vector3.Down * 100f;
traceResults[3] = Scene.Trace.Ray( pos, to )
.WithAnyTags( "solid", "skateable" )
.WithoutTags( "unskateable" )
.Run();
bool closestSet = false;
float closestDistance = 0f;
var closestPosition = Vector3.Zero;
for ( int i = 0; i < traceResults.Length; i++ )
{
var traceResult = traceResults[i];
if ( !traceResult.Hit )
continue;
var upFrom = Position + Vector3.Up * 50f;
var upTo = traceResult.EndPosition + Vector3.Up * 50f;
var blocked = Scene.Trace.Ray( upFrom, upTo )
.WithAnyTags( "solid", "playerclip", "passbullets", "unskateable" )
.Run()
.Hit;
if ( blocked )
continue;
float dist = Vector3.DistanceBetween( Position, traceResult.EndPosition );
if ( !closestSet || dist < closestDistance )
{
closestSet = true;
closestDistance = dist;
closestPosition = traceResult.EndPosition;
}
}
if ( closestSet )
helper.Position = closestPosition;
}
helper.Velocity = Velocity;
}
if ( helper.HitWall && !OnGrind && !helper.HitPhysics )
{
if ( IsGrounded )
{
realRot = Rotation.LookAt( helper.Velocity.WithZ( 0f ), realRot.Up );
realRot = MathLD.FromToRotation( Vector3.Up * realRot, GroundNormal ) * realRot;
}
if ( Velocity.Dot( -helper.HitNormal ) >= HitForce )
{
if ( IsGrounded )
{
Rotation = realRot;
SnapRotation = true;
}
if ( Velocity.Length > StoppedVelocity )
{
var hitForward = Vector3.Dot( Velocity.Normal, realRot.Forward ) >= 0f;
Animator?.HandleEvent( hitForward ? "front" : "back" );
}
Pawn.PlaySound( "body_hit", WorldPosition );
if ( Pawn.HasHelmet() )
Pawn.PlaySound( "helmet_hit", WorldPosition );
}
}
Position = helper.Position;
Velocity = helper.Velocity;
}
else
{
var a = Position + Vector3.Up * 10f;
var b = a + Velocity * Time.Delta;
var sanity = Scene.Trace.Ray( a, b )
.WithAnyTags( "solid", "playerclip", "passbullets", "unskateable" )
.Run();
if ( !sanity.Hit )
{
Position += Velocity * Time.Delta;
}
else
{
Position += sanity.Normal * 20f;
Velocity -= Vector3.VectorPlaneProject( Velocity, -sanity.Normal );
}
}
Rotation = Rotation.Lerp( Rotation, realRot, SmoothingSpeed * Time.Delta );
if ( IsGrounded )
{
var tracePos = Position + realRot.Up * wallCollisionSphereHeight + Vector3.Up * wallCollisionSphereGroundDistance;
var sphereResult = Scene.Trace.Sphere( wallCollisionSphereRadius, tracePos, tracePos )
.WithAnyTags( "solid", "playerclip", "passbullets", "unskateable" )
.Run();
if ( sphereResult.Hit )
{
var heading = (sphereResult.HitPosition - tracePos).Normal;
heading -= heading.ProjectOnNormal( GroundNormal );
heading = heading.Normal;
var distance = Vector3.DistanceBetween( tracePos, sphereResult.HitPosition );
Position -= heading * (distance + wallCollisionSphereForce);
if ( Velocity.Dot( heading ) > 0f )
Velocity -= Velocity.ProjectOnNormal( heading );
}
}
var groundVector = IsGrounded ? GroundNormal : Vector3.Up;
bool wasOnGround = IsGrounded;
float currentTrace = IsGrounded ? 3f : 2f;
var traceOffset1 = groundVector * 25f;
var traceOffset2 = -(groundVector * currentTrace);
if ( !IsGrounded )
{
var rotationOffset = realRot.Up * rotationTraceOffset;
rotationOffset.z = 0f;
traceOffset1 = groundVector * 25f + rotationOffset;
if ( Vector3.Dot( realRot.Up, Vector3.Down ) >= upsideDownDotThreshold )
currentTrace += upsideDownTraceOffset;
traceOffset2 = -(groundVector * currentTrace) + rotationOffset;
}
if ( !OnGrind )
{
var from = Position + traceOffset1;
var to = Position + traceOffset2;
var floorTrace = Scene.Trace.Ray( from, to )
.WithAnyTags( "solid", "playerclip", "passbullets", "vert", "skateable", "unskateable" )
.WithoutTags( "dynamicprop" );
var floor = floorTrace.Run();
if ( floor.GameObject == null || HasTag( floor, "unskateable" ) )
{
ClearGroundEntity( ref realRot );
}
else
{
bool isVert = HasTag( floor, "vert" ) && IsValidVert( floor.Normal );
bool canStand = false;
if ( !floor.StartedSolid && floor.Fraction > 0f && floor.Fraction < 1f )
{
if ( floor.Normal.Angle( Vector3.Up ) < MaxStandableAngle )
canStand = true;
if ( HasTag( floor, "vert" ) || HasTag( floor, "skateable" ) )
canStand = true;
}
if ( !IsGrounded && Velocity.Normal.Dot( -floor.Normal ) < 0f )
canStand = false;
if ( canStand && IsGrounded )
{
float slopeChange = floor.Normal.Angle( GroundNormal );
float dotFw = Vector3.Dot( floor.Normal, Velocity.Normal );
if ( slopeChange >= MaxSlope && dotFw > 0f )
canStand = false;
}
if ( canStand )
{
float oldForwardSpeed = Velocity.Dot( realRot.Forward );
var prevGround = GroundObject;
UpdateGroundEntity( floor );
if ( Vector3.Dot( Velocity, -GroundNormal ) >= 100f && prevGround == null )
Animator?.HandleEvent( "land" );
var oldRot = realRot;
realRot = MathLD.FromToRotation( Vector3.Up * realRot, GroundNormal ) * realRot;
if ( prevGround == null )
{
bool bailed = false;
bool awkward = false;
float angleDiff = oldRot.Up.Normal.Angle( floor.Normal );
if ( angleDiff > LandBailMaxAngle )
{
awkward = true;
Pawn.Bail( SkatePawn.BailType.Landing );
bailed = true;
}
float sidewaysVel = Math.Abs( Vector3.Dot( (Velocity.Length > 800f) ? (800f * Velocity.Normal) : Velocity, realRot.Right ) );
if ( sidewaysVel >= SidewaysBailSpeed && !bailed )
Pawn.Bail();
if ( Vector3.Dot( Velocity, realRot.Forward ) < 0f && Velocity.Length > StoppedVelocity )
realRot = Rotation.LookAt( realRot.Backward, GroundNormal );
if ( !awkward )
Pawn.PlaySound( "skate_land", WorldPosition );
SnapRotation = true;
Rotation = realRot;
float ang = Vector3.Dot( GroundNormal, Vector3.Up );
Velocity *= MathX.Lerp( LandSpeedMultiplier, 1f, ang );
}
else
{
Velocity = oldForwardSpeed * realRot.Forward;
}
realRot = Rotation.LookAt( realRot.Forward, GroundNormal );
Velocity -= Velocity.ProjectOnNormal( floor.Normal );
Position = floor.EndPosition + floor.Normal * 1f;
GroundVert = isVert;
TrickScores.Finished = true;
}
else
{
ClearGroundEntity( ref realRot );
}
}
}
pawn.RealRotation = realRot;
pawn.OnVert = OnVert;
Pawn.Velocity = Velocity;
if ( _currentCoyoteTime > 0f )
_currentCoyoteTime -= Time.Delta;
else
_jumped = false;
if ( IsGrounded )
{
_currentCoyoteTime = 0f;
_jumped = false;
}
if ( wasOnGround && !IsGrounded )
_currentCoyoteTime = CoyoteTime;
if ( jump )
{
var jumpDir = Vector3.Up;
var angle = GroundNormal.Angle( Vector3.Up );
var jumpOffset = Vector3.Zero;
if ( angle > 90f )
{
jumpDir *= -1f;
jumpOffset = jumpDir * 20f;
}
if ( OnGrind )
{
jumpOffset = jumpDir * 20f;
Velocity += 200f * realRot.Left * DigitalLeftInput;
}
ClearGroundEntity( ref realRot );
_currentJumpStrength = MathX.Lerp( _currentJumpStrength, JumpStrengthMinimum, Velocity.z / JumpMaxSpeed );
Velocity += jumpDir * JumpForce * _currentJumpStrength;
Position += jumpOffset;
_jumped = true;
_currentJumpStrength = JumpStrengthMinimum;
Animator?.HandleEvent( "jump" );
Pawn.PlaySound( "ollie", WorldPosition );
Pawn.PlaySound( "jump", WorldPosition );
StopGrind( pawn, manual: true );
}
if ( JumpReady )
{
_currentJumpStrength = (_currentJumpStrength < 1f)
? (_currentJumpStrength + Time.Delta * JumpStrengthSpeed)
: 1f;
}
ClampTopSpeed();
}
private static bool HasTag( SceneTraceResult tr, string tag )
{
if ( tr.Tags is not null && tr.Tags.Contains( tag, StringComparer.OrdinalIgnoreCase ) )
return true;
return tr.GameObject?.Tags.Has( tag ) ?? false;
}
public void ResetState()
{
if ( Pawn is null )
return;
var r = Pawn.RealRotation;
ClearGroundEntity( ref r );
OnVert = false;
HasVertBelow = false;
Pushing = false;
SpunAmount = 0f;
OnGrind = false;
Velocity = Vector3.Zero;
AngularVelocity = Angles.Zero;
Pawn.RealRotation = Rotation;
Pawn.Velocity = Vector3.Zero;
}
}