using Sandbox;
using System;
using System.Collections.Generic;
namespace MurderGame;
public partial class WalkControllerComponent : BaseControllerComponent
{
public bool CanSprint
{
get
{
return TeamCapabilities.CanSprint( Entity.Team );
}
}
[Net] public float SprintSpeed { get; set; } = 320.0f;
[Net] public float WalkSpeed { get; set; } = 150.0f;
[Net] public float CrouchSpeed { get; set; } = 80.0f;
[Net] public float DefaultSpeed { get; set; } = 190.0f;
[Net] public float Acceleration { get; set; } = 10.0f;
[Net] public float AirAcceleration { get; set; } = 50.0f;
[Net] public float FallSoundZ { get; set; } = -30.0f;
[Net] public float GroundFriction { get; set; } = 4.0f;
[Net] public float StopSpeed { get; set; } = 100.0f;
[Net] public float Size { get; set; } = 20.0f;
[Net] public float DistEpsilon { get; set; } = 0.03125f;
[Net] public float GroundAngle { get; set; } = 46.0f;
[Net] public float Bounce { get; set; } = 0.0f;
[Net] public float MoveFriction { get; set; } = 1.0f;
[Net] public float StepSize { get; set; } = 18.0f;
[Net] public float MaxNonJumpVelocity { get; set; } = 140.0f;
[Net] public float BodyGirth { get; set; } = 32.0f;
[Net] public float BodyHeight { get; set; } = 72.0f;
[Net] public float EyeHeight { get; set; } = 64.0f;
[Net] public float DuckHeight { get; set; } = 32.0f;
[Net] public float DuckEyeHeight { get; set; } = 24.0f;
[Net] public float Gravity { get; set; } = 800.0f;
[Net] public float AirControl { get; set; } = 30.0f;
[Net] public float SpeedMultiplier { get; set; } = 1f;
[ConVar.Replicated( "walkcontroller_showbbox" )] public bool ShowBBox { get; set; } = false;
public bool Swimming { get; set; } = false;
[Net] public bool AutoJump { get; set; } = false;
public TimeSince TimeSinceFootstep { get; set; }
public bool FootstepFoot { get; set; }
public WalkControllerComponent()
{
}
///
/// This is temporary, get the hull size for the player's collision
///
public BBox GetHull()
{
var girth = BodyGirth * 0.5f;
var height = BodyHeight;
if ( IsDucking ) height = 32;
var mins = new Vector3( -girth, -girth, 0 );
var maxs = new Vector3( +girth, +girth, BodyHeight );
return new BBox( mins, maxs );
}
// Duck body height 32
// Eye Height 64
// Duck Eye Height 28
public Vector3 mins;
public Vector3 maxs;
///
/// Any bbox traces we do will be offset by this amount.
/// todo: this needs to be predicted
///
public Vector3 TraceOffset;
public virtual void SetBBox( Vector3 mins, Vector3 maxs )
{
if ( this.mins == mins && this.maxs == maxs )
return;
this.mins = mins;
this.maxs = maxs;
}
///
/// Update the size of the bbox. We should really trigger some shit if this changes.
///
public virtual void UpdateBBox( int forceduck = 0 )
{
var girth = BodyGirth * 0.5f;
var height = (EyeHeight + DuckAmount) + (BodyHeight - EyeHeight);
if ( forceduck == 1 ) height = DuckHeight;
if ( forceduck == -1 ) height = BodyHeight;
var mins = new Vector3( -girth, -girth, 0 ) * Entity.Scale;
var maxs = new Vector3( +girth, +girth, height ) * Entity.Scale;
SetBBox( mins, maxs );
}
protected float SurfaceFriction;
public override void FrameSimulate( IClient cl )
{
if ( ShowBBox ) DebugOverlay.Box( Entity.Position, mins, maxs, Color.Yellow );
RestoreGroundAngles();
var pl = Entity as Player;
SaveGroundAngles();
DuckFrameSimulate();
}
public override void BuildInput()
{
var pl = Entity as Player;
pl.InputDirection = Input.AnalogMove;
}
public override void Simulate( IClient cl )
{
var pl = Entity as Player;
Events?.Clear();
Tags?.Clear();
pl.EyeLocalPosition = Vector3.Up * EyeHeight;
pl.EyeRotation = pl.ViewAngles.ToRotation();
CheckDuck();
UpdateBBox();
RestoreGroundPos();
//Entity.Velocity += Entity.BaseVelocity * (1 + Time.Delta * 0.5f);
//Entity.BaseVelocity = Vector3.Zero;
//Rot = Rotation.LookAt( Input.Rotation.Forward.WithZ( 0 ), Vector3.Up );
// Check Stuck
// Unstuck - or return if stuck
// Set Ground Entity to null if falling faster then 250
// store water level to compare later
// if not on ground, store fall velocity
// player->UpdateStepSound( player->m_pSurfaceData, mv->GetAbsOrigin(), mv->m_vecVelocity )
// RunLadderMode
CheckLadder();
Swimming = Entity.GetWaterLevel() > 0.5f;
//
// Start Gravity
//
if ( !Swimming && !IsTouchingLadder )
{
Entity.Velocity -= new Vector3( 0, 0, (Gravity * Entity.Scale) * 0.5f ) * Time.Delta;
Entity.Velocity += new Vector3( 0, 0, Entity.BaseVelocity.z ) * Time.Delta;
Entity.BaseVelocity = Entity.BaseVelocity.WithZ( 0 );
}
/*
if (player->m_flWaterJumpTime)
{
WaterJump();
TryPlayerMove();
// See if we are still in water?
CheckWater();
return;
}
*/
// if ( underwater ) do underwater movement
if ( Entity.GetWaterLevel() > 0.1 )
{
WaterSimulate();
}
else
if ( AutoJump ? Input.Down( "Jump" ) : Input.Pressed( "Jump" ) )
{
CheckJumpButton();
}
// Fricion is handled before we add in any base velocity. That way, if we are on a conveyor,
// we don't slow when standing still, relative to the conveyor.
bool bStartOnGround = Entity.GroundEntity != null;
//bool bDropSound = false;
if ( bStartOnGround )
{
//if ( Velocity.z < FallSoundZ ) bDropSound = true;
Entity.Velocity = Entity.Velocity.WithZ( 0 );
//player->m_Local.m_flFallVelocity = 0.0f;
if ( Entity.GroundEntity != null )
{
ApplyFriction( GroundFriction * SurfaceFriction );
}
}
//
// Work out wish velocity.. just take input, rotate it to view, clamp to -1, 1
//
WishVelocity = new Vector3( pl.InputDirection.x.Clamp( -1f, 1f ), pl.InputDirection.y.Clamp( -1f, 1f ), 0 );
var inSpeed = WishVelocity.Length.Clamp( 0, 1 );
if ( Swimming || IsTouchingLadder )
{
WishVelocity *= pl.ViewAngles.ToRotation();
}
else
{
WishVelocity *= pl.ViewAngles.WithPitch( 0 ).ToRotation();
}
if ( !Swimming && !IsTouchingLadder )
{
WishVelocity = WishVelocity.WithZ( 0 );
}
WishVelocity = WishVelocity.Normal * inSpeed;
WishVelocity *= GetWishSpeed();
WishVelocity *= Entity.Scale;
WishVelocity *= SpeedMultiplier;
bool bStayOnGround = false;
if ( Swimming )
{
ApplyFriction( 1 );
WaterMove();
}
else if ( IsTouchingLadder )
{
Entity.Tags.Add( "climbing" );
LadderMove();
}
else if ( Entity.GroundEntity != null )
{
bStayOnGround = true;
WalkMove();
}
else
{
AirMove();
}
CategorizePosition( bStayOnGround );
// FinishGravity
if ( !Swimming && !IsTouchingLadder )
{
Entity.Velocity -= new Vector3( 0, 0, (Gravity * Entity.Scale) * 0.5f ) * Time.Delta;
}
if ( Entity.GroundEntity != null )
{
Entity.Velocity = Entity.Velocity.WithZ( 0 );
}
DoPushingStuff();
if ( Entity == null ) return;
// CheckFalling(); // fall damage etc
// Land Sound
// Swim Sounds
SaveGroundPos();
LatchOntoLadder();
PreviousGroundEntity = Entity.GroundEntity;
bool Debug = false;
if ( Debug )
{
DebugOverlay.Box( Entity.Position + TraceOffset, mins, maxs, Color.Red );
DebugOverlay.Box( Entity.Position, mins, maxs, Color.Blue );
var lineOffset = 0;
if ( Game.IsServer ) lineOffset = 10;
DebugOverlay.ScreenText( $" Position: {Entity.Position}", lineOffset + 0 );
DebugOverlay.ScreenText( $" Velocity: {Entity.Velocity}", lineOffset + 1 );
DebugOverlay.ScreenText( $" BaseVelocity: {Entity.BaseVelocity}", lineOffset + 2 );
DebugOverlay.ScreenText( $" GroundEntity: {Entity.GroundEntity} [{Entity.GroundEntity?.Velocity}]", lineOffset + 3 );
DebugOverlay.ScreenText( $" SurfaceFriction: {SurfaceFriction}", lineOffset + 4 );
DebugOverlay.ScreenText( $" WishVelocity: {WishVelocity}", lineOffset + 5 );
DebugOverlay.ScreenText( $" Speed: {Entity.Velocity.Length}", lineOffset + 6 );
}
}
public virtual float GetWishSpeed()
{
var ws = -1;// Duck.GetWishSpeed();
if ( ws >= 0 ) return ws;
if ( Input.Down( "Duck" ) || IsDucking ) return CrouchSpeed;
if ( Input.Down( "Run" ) && CanSprint ) return SprintSpeed;
if ( Input.Down( "Walk" ) ) return WalkSpeed;
return DefaultSpeed;
}
public virtual void WalkMove()
{
var wishdir = WishVelocity.Normal;
var wishspeed = WishVelocity.Length;
WishVelocity = WishVelocity.WithZ( 0 );
WishVelocity = WishVelocity.Normal * wishspeed;
Entity.Velocity = Entity.Velocity.WithZ( 0 );
Accelerate( wishdir, wishspeed, 0, Acceleration );
Entity.Velocity = Entity.Velocity.WithZ( 0 );
// Player.SetAnimParam( "forward", Input.Forward );
// Player.SetAnimParam( "sideward", Input.Right );
// Player.SetAnimParam( "wishspeed", wishspeed );
// Player.SetAnimParam( "walkspeed_scale", 2.0f / 190.0f );
// Player.SetAnimParam( "runspeed_scale", 2.0f / 320.0f );
// DebugOverlay.Text( 0, Pos + Vector3.Up * 100, $"forward: {Input.Forward}\nsideward: {Input.Right}" );
// Add in any base velocity to the current velocity.
if (Entity.Velocity.Length != 0)
{
var footstepTimeThreshold = 60f / Entity.Velocity.Length;
if (TimeSinceFootstep > footstepTimeThreshold)
{
var trace = Trace.Ray( Entity.Position, Entity.Position + Vector3.Down * 20f )
.Radius( 1f )
.Ignore( Entity )
.Run();
if (trace.Hit)
{
FootstepFoot = !FootstepFoot;
trace.Surface.DoFootstep( Entity, trace, FootstepFoot ? 1 : 0, 35f * (Entity.Velocity.Length / SprintSpeed) );
TimeSinceFootstep = 0;
}
}
}
Entity.Velocity += Entity.BaseVelocity;
try
{
if ( Entity.Velocity.Length < 1.0f )
{
Entity.Velocity = Vector3.Zero;
return;
}
// first try just moving to the destination
var dest = (Entity.Position + Entity.Velocity * Time.Delta).WithZ( Entity.Position.z );
var pm = TraceBBox( Entity.Position, dest );
if ( pm.Fraction == 1 )
{
Entity.Position = pm.EndPosition;
StayOnGround();
return;
}
StepMove();
}
finally
{
// Now pull the base velocity back out. Base velocity is set if you are on a moving object, like a conveyor (or maybe another monster?)
Entity.Velocity -= Entity.BaseVelocity;
}
StayOnGround();
Entity.Velocity = Entity.Velocity.Normal * MathF.Min( Entity.Velocity.Length, (GetWishSpeed() * Entity.Scale) );
}
public virtual void StepMove()
{
MoveHelper mover = new MoveHelper( Entity.Position, Entity.Velocity );
mover.Trace = mover.Trace.Size( mins, maxs ).Ignore( Entity );
mover.MaxStandableAngle = GroundAngle;
mover.TryMoveWithStep( Time.Delta, StepSize * Entity.Scale );
Entity.Position = mover.Position;
Entity.Velocity = mover.Velocity;
}
public virtual void Move()
{
MoveHelper mover = new MoveHelper( Entity.Position, Entity.Velocity );
mover.Trace = mover.Trace.Size( mins, maxs ).Ignore( Entity );
mover.MaxStandableAngle = GroundAngle;
mover.TryMove( Time.Delta );
Entity.Position = mover.Position;
Entity.Velocity = mover.Velocity;
}
///
/// Add our wish direction and speed onto our velocity
///
public virtual void Accelerate( Vector3 wishdir, float wishspeed, float speedLimit, float acceleration )
{
// This gets overridden because some games (CSPort) want to allow dead (observer) players
// to be able to move around.
// if ( !CanAccelerate() )
// return;
speedLimit *= Entity.Scale;
acceleration /= Entity.Scale;
if ( speedLimit > 0 && wishspeed > speedLimit )
wishspeed = speedLimit;
// See if we are changing direction a bit
var currentspeed = Entity.Velocity.Dot( wishdir );
// Reduce wishspeed by the amount of veer.
var addspeed = wishspeed - currentspeed;
// If not going to add any speed, done.
if ( addspeed <= 0 )
return;
// Determine amount of acceleration.
var accelspeed = (acceleration * Entity.Scale) * Time.Delta * wishspeed * SurfaceFriction;
// Cap at addspeed
if ( accelspeed > addspeed )
accelspeed = addspeed;
Entity.Velocity += wishdir * accelspeed;
}
///
/// Remove ground friction from velocity
///
public virtual void ApplyFriction( float frictionAmount = 1.0f )
{
// If we are in water jump cycle, don't apply friction
//if ( player->m_flWaterJumpTime )
// return;
// Not on ground - no friction
// Calculate speed
var speed = Entity.Velocity.Length;
if ( speed < 0.1f ) return;
// Bleed off some speed, but if we have less than the bleed
// threshold, bleed the threshold amount.
float control = (speed < StopSpeed * Entity.Scale) ? (StopSpeed * Entity.Scale) : speed;
// Add the amount to the drop amount.
var drop = control * Time.Delta * frictionAmount;
// scale the velocity
float newspeed = speed - drop;
if ( newspeed < 0 ) newspeed = 0;
if ( newspeed != speed )
{
newspeed /= speed;
Entity.Velocity *= newspeed;
}
// mv->m_outWishVel -= (1.f-newspeed) * mv->m_vecVelocity;
}
[Net, Predicted] public bool IsDucking { get; set; } // replicate
[Net, Predicted] public float DuckAmount { get; set; } = 0;
public virtual void CheckDuck()
{
var pl = Entity as Player;
bool wants = Input.Down( "Duck" );
if ( wants != IsDucking )
{
if ( wants ) TryDuck();
else TryUnDuck();
}
if ( IsDucking )
{
var delta = DuckAmount;
DuckAmount = DuckAmount.LerpTo( (EyeHeight - DuckEyeHeight) * -1, 8 * Time.Delta );
delta -= DuckAmount;
SetTag( "ducked" );
if ( pl.GroundEntity is null )
{
pl.Position += Vector3.Up * (delta * Entity.Scale);
var pm = TraceBBox( Entity.Position, Entity.Position );
if ( pm.StartedSolid )
{
pl.Position -= Vector3.Up * (delta * Entity.Scale);
}
}
FixPlayerCrouchStuck( true );
CategorizePosition( false );
}
else
{
var delta = DuckAmount;
DuckAmount = DuckAmount.LerpTo( 0, 8 * Time.Delta );
delta -= DuckAmount;
if ( pl.GroundEntity is null )
{
pl.Position += Vector3.Up * (delta * Entity.Scale);
var pm = TraceBBox( Entity.Position, Entity.Position );
if ( pm.StartedSolid )
{
pl.Position -= Vector3.Up * (delta * Entity.Scale);
}
}
CategorizePosition( false );
}
pl.EyeLocalPosition = pl.EyeLocalPosition.WithZ( EyeHeight + (DuckAmount) );
}
public float LocalDuckAmount { get; set; } = 0;
void DuckFrameSimulate()
{
var pl = Entity as Player;
if ( IsDucking )
{
LocalDuckAmount = LocalDuckAmount.LerpTo( (EyeHeight - DuckEyeHeight) * -1, 8 * Time.Delta );
}
else
{
LocalDuckAmount = LocalDuckAmount.LerpTo( 0, 8 * Time.Delta );
}
pl.EyeLocalPosition = pl.EyeLocalPosition.WithZ( EyeHeight + LocalDuckAmount );
}
public virtual void TryDuck()
{
IsDucking = true;
}
public virtual void TryUnDuck()
{
UpdateBBox( -1 );
var pm = TraceBBox( Entity.Position, Entity.Position, DuckHeight - 4 );
if ( pm.StartedSolid )
{
UpdateBBox();
return;
}
IsDucking = false;
}
public virtual void FixPlayerCrouchStuck( bool upward )
{
int direction = upward ? 1 : 0;
var trace = TraceBBox( Entity.Position, Entity.Position );
if ( trace.Entity == null )
return;
var test = Entity.Position;
for ( int i = 0; i < (DuckHeight - 4); i++ )
{
var org = Entity.Position;
org.z += direction;
Entity.Position = org;
trace = TraceBBox( Entity.Position, Entity.Position );
if ( trace.Entity == null )
return;
}
Entity.Position = test;
}
public virtual void CheckJumpButton()
{
//if ( !player->CanJump() )
// return false;
/*
if ( player->m_flWaterJumpTime )
{
player->m_flWaterJumpTime -= gpGlobals->frametime();
if ( player->m_flWaterJumpTime < 0 )
player->m_flWaterJumpTime = 0;
return false;
}*/
// If we are in the water most of the way...
if ( Swimming )
{
// swimming, not jumping
ClearGroundEntity();
Entity.Velocity = Entity.Velocity.WithZ( 100 );
// play swimming sound
// if ( player->m_flSwimSoundTime <= 0 )
{
// Don't play sound again for 1 second
// player->m_flSwimSoundTime = 1000;
// PlaySwimSound();
}
return;
}
if ( Entity.GroundEntity == null )
return;
/*
if ( player->m_Local.m_bDucking && (player->GetFlags() & FL_DUCKING) )
return false;
*/
/*
// Still updating the eye position.
if ( player->m_Local.m_nDuckJumpTimeMsecs > 0u )
return false;
*/
ClearGroundEntity();
// player->PlayStepSound( (Vector &)mv->GetAbsOrigin(), player->m_pSurfaceData, 1.0, true );
// MoveHelper()->PlayerSetAnimation( PLAYER_JUMP );
float flGroundFactor = 1.0f;
//if ( player->m_pSurfaceData )
{
// flGroundFactor = g_pPhysicsQuery->GetGameSurfaceproperties( player->m_pSurfaceData )->m_flJumpFactor;
}
float flMul = (268.3281572999747f * Entity.Scale) * 1.2f;
float startz = Entity.Velocity.z;
Entity.Velocity = Entity.Velocity.WithZ( startz + flMul * flGroundFactor );
Entity.Velocity -= new Vector3( 0, 0, (Gravity * Entity.Scale) * 0.5f ) * Time.Delta;
// mv->m_outJumpVel.z += mv->m_vecVelocity[2] - startz;
// mv->m_outStepHeight += 0.15f;
// don't jump again until released
//mv->m_nOldButtons |= IN_JUMP;
AddEvent( "jump" );
}
public virtual void WaterSimulate()
{
if ( Entity.GetWaterLevel() > 0.4 )
{
CheckWaterJump();
}
// If we are falling again, then we must not trying to jump out of water any more.
if ( (Entity.Velocity.z < 0.0f) && IsJumpingFromWater )
{
WaterJumpTime = 0.0f;
}
// Was jump button pressed?
if ( Input.Down( "Jump" ) )
{
CheckJumpButton();
}
SetTag( "swimming" );
}
protected float WaterJumpTime { get; set; }
protected Vector3 WaterJumpVelocity { get; set; }
protected bool IsJumpingFromWater => WaterJumpTime > 0;
protected TimeSince TimeSinceSwimSound { get; set; }
protected float LastWaterLevel { get; set; }
public virtual float WaterJumpHeight => 8;
protected void CheckWaterJump()
{
// Already water jumping.
if ( IsJumpingFromWater )
return;
// Don't hop out if we just jumped in
// only hop out if we are moving up
if ( Entity.Velocity.z < -180 )
return;
// See if we are backing up
var flatvelocity = Entity.Velocity.WithZ( 0 );
// Must be moving
var curspeed = flatvelocity.Length;
flatvelocity = flatvelocity.Normal;
// see if near an edge
var flatforward = Entity.Rotation.Forward.WithZ( 0 ).Normal;
// Are we backing into water from steps or something? If so, don't pop forward
if ( curspeed != 0 && Vector3.Dot( flatvelocity, flatforward ) < 0 )
return;
var vecStart = Entity.Position + (mins + maxs) * .5f;
var vecEnd = vecStart + flatforward * 24;
var tr = TraceBBox( vecStart, vecEnd );
if ( tr.Fraction == 1 )
return;
vecStart.z = Entity.Position.z + EyeHeight + WaterJumpHeight;
vecEnd = vecStart + flatforward * 24;
WaterJumpVelocity = tr.Normal * -50;
tr = TraceBBox( vecStart, vecEnd );
if ( tr.Fraction < 1.0 )
return;
// Now trace down to see if we would actually land on a standable surface.
vecStart = vecEnd;
vecEnd.z -= 1024;
tr = TraceBBox( vecStart, vecEnd );
if ( tr.Fraction < 1 && tr.Normal.z >= 0.7f )
{
Entity.Velocity = Entity.Velocity.WithZ( 256 * Entity.Scale );
Entity.Tags.Add( "waterjump" );
WaterJumpTime = 2000;
}
}
public virtual void AirMove()
{
var wishdir = WishVelocity.Normal;
var wishspeed = WishVelocity.Length;
Accelerate( wishdir, wishspeed, AirControl, AirAcceleration );
Entity.Velocity += Entity.BaseVelocity;
Move();
Entity.Velocity -= Entity.BaseVelocity;
}
public virtual void WaterMove()
{
var wishdir = WishVelocity.Normal;
var wishspeed = WishVelocity.Length;
wishspeed *= 0.8f;
Accelerate( wishdir, wishspeed, 100 * Entity.Scale, Acceleration );
Entity.Velocity += Entity.BaseVelocity;
Move();
Entity.Velocity -= Entity.BaseVelocity;
}
bool IsTouchingLadder = false;
Vector3 LadderNormal;
Vector3 LastNonZeroWishLadderVelocity;
public virtual void CheckLadder()
{
var pl = Entity as Player;
var wishvel = new Vector3( pl.InputDirection.x.Clamp( -1f, 1f ), pl.InputDirection.y.Clamp( -1f, 1f ), 0 );
if ( wishvel.Length > 0 )
{
LastNonZeroWishLadderVelocity = wishvel;
}
if ( TryLatchNextTickCounter > 0 ) wishvel = LastNonZeroWishLadderVelocity * -1;
wishvel *= pl.ViewAngles.WithPitch( 0 ).ToRotation();
wishvel = wishvel.Normal;
if ( IsTouchingLadder )
{
if ( Input.Pressed( "Jump" ) )
{
var sidem = (Math.Abs( Entity.ViewAngles.ToRotation().Forward.Abs().z - 1 ) * 3).Clamp( 0, 1 );
var upm = Entity.ViewAngles.ToRotation().Forward.z;
var Eject = new Vector3();
Eject.x = LadderNormal.x * sidem;
Eject.y = LadderNormal.y * sidem;
Eject.z = (3 * upm).Clamp( 0, 1 );
Entity.Velocity += (Eject * 180.0f) * Entity.Scale;
IsTouchingLadder = false;
return;
}
else if ( Entity.GroundEntity != null && LadderNormal.Dot( wishvel ) > 0 )
{
IsTouchingLadder = false;
return;
}
}
const float ladderDistance = 1.0f;
var start = Entity.Position;
Vector3 end = start + (IsTouchingLadder ? (LadderNormal * -1.0f) : wishvel) * ladderDistance;
var pm = Trace.Ray( start, end )
.Size( mins, maxs )
.WithTag( "ladder" )
.Ignore( Entity )
.Run();
IsTouchingLadder = false;
if ( pm.Hit )
{
IsTouchingLadder = true;
LadderNormal = pm.Normal;
}
}
public virtual void LadderMove()
{
var velocity = WishVelocity;
float normalDot = velocity.Dot( LadderNormal );
var cross = LadderNormal * normalDot;
Entity.Velocity = (velocity - cross) + (-normalDot * LadderNormal.Cross( Vector3.Up.Cross( LadderNormal ).Normal ));
Move();
}
Entity PreviousGroundEntity;
int TryLatchNextTickCounter = 0;
Vector3 LastNonZeroWishVelocity;
[ConVar.Replicated( "sv_ladderlatchdebug" )]
public static bool LatchDebug { get; set; } = false;
public virtual void LatchOntoLadder()
{
if ( !WishVelocity.Normal.IsNearlyZero( 0.001f ) )
{
LastNonZeroWishVelocity = WishVelocity;
}
if ( TryLatchNextTickCounter > 0 )
{
Entity.Velocity = (LastNonZeroWishVelocity.Normal * -100).WithZ( Entity.Velocity.z );
TryLatchNextTickCounter++;
}
if ( TryLatchNextTickCounter >= 10 )
{
TryLatchNextTickCounter = 0;
}
if ( Entity.GroundEntity != null ) return;
if ( PreviousGroundEntity == null ) return;
var pos = Entity.Position + (Vector3.Down * 16);
//var tr = TraceBBox( pos, pos );
var tr = Trace.Ray( pos, pos - (LastNonZeroWishVelocity.Normal * 8) )
.Size( mins, maxs )
.WithTag( "ladder" )
.Ignore( Entity )
.Run();
if ( LatchDebug ) DebugOverlay.Line( Entity.Position, pos, 10 );
if ( LatchDebug ) DebugOverlay.Line( tr.StartPosition, tr.EndPosition, 10 );
if ( tr.Hit )
{
Entity.Velocity = Vector3.Zero.WithZ( Entity.Velocity.z );
TryLatchNextTickCounter++;
}
}
public virtual void CategorizePosition( bool bStayOnGround )
{
SurfaceFriction = 1.0f;
// Doing this before we move may introduce a potential latency in water detection, but
// doing it after can get us stuck on the bottom in water if the amount we move up
// is less than the 1 pixel 'threshold' we're about to snap to. Also, we'll call
// this several times per frame, so we really need to avoid sticking to the bottom of
// water on each call, and the converse case will correct itself if called twice.
//CheckWater();
var point = Entity.Position - Vector3.Up * (2 * Entity.Scale);
var vBumpOrigin = Entity.Position;
//
// Shooting up really fast. Definitely not on ground trimed until ladder shit
//
bool bMovingUpRapidly = Entity.Velocity.z > MaxNonJumpVelocity;
bool bMovingUp = Entity.Velocity.z > 0;
bool bMoveToEndPos = false;
if ( Entity.GroundEntity != null ) // and not underwater
{
bMoveToEndPos = true;
point.z -= StepSize * Entity.Scale;
}
else if ( bStayOnGround )
{
bMoveToEndPos = true;
point.z -= StepSize * Entity.Scale;
}
if ( bMovingUpRapidly || Swimming ) // or ladder and moving up
{
ClearGroundEntity();
return;
}
var pm = TraceBBox( vBumpOrigin, point, 4.0f );
if ( pm.Entity == null || Vector3.GetAngle( Vector3.Up, pm.Normal ) > GroundAngle )
{
ClearGroundEntity();
bMoveToEndPos = false;
if ( Entity.Velocity.z > 0 )
SurfaceFriction = 0.25f;
}
else
{
UpdateGroundEntity( pm );
}
if ( bMoveToEndPos && !pm.StartedSolid && pm.Fraction > 0.0f && pm.Fraction < 1.0f )
{
Entity.Position = pm.EndPosition;
}
}
public Vector3 GroundNormal { get; set; }
///
/// We have a new ground entity
///
public virtual void UpdateGroundEntity( TraceResult tr )
{
GroundNormal = tr.Normal;
// VALVE HACKHACK: Scale this to fudge the relationship between vphysics friction values and player friction values.
// A value of 0.8f feels pretty normal for vphysics, whereas 1.0f is normal for players.
// This scaling trivially makes them equivalent. REVISIT if this affects low friction surfaces too much.
SurfaceFriction = tr.Surface.Friction * 1.25f;
if ( SurfaceFriction > 1 ) SurfaceFriction = 1;
//if ( tr.Entity == GroundEntity ) return;
Vector3 oldGroundVelocity = default;
if ( Entity.GroundEntity != null ) oldGroundVelocity = Entity.GroundEntity.Velocity;
bool wasOffGround = Entity.GroundEntity == null;
Entity.GroundEntity = tr.Entity;
if ( Entity.GroundEntity != null )
{
Entity.BaseVelocity = Entity.GroundEntity.Velocity;
}
}
///
/// We're no longer on the ground, remove it
///
public virtual void ClearGroundEntity()
{
if ( Entity.GroundEntity == null ) return;
Entity.GroundEntity = null;
GroundNormal = Vector3.Up;
SurfaceFriction = 1.0f;
}
///
/// Traces the current bbox and returns the result.
/// liftFeet will move the start position up by this amount, while keeping the top of the bbox at the same
/// position. This is good when tracing down because you won't be tracing through the ceiling above.
///
public virtual TraceResult TraceBBox( Vector3 start, Vector3 end, float liftFeet = 0.0f )
{
return TraceBBox( start, end, mins, maxs, liftFeet );
}
///
/// Traces the bbox and returns the trace result.
/// LiftFeet will move the start position up by this amount, while keeping the top of the bbox at the same
/// position. This is good when tracing down because you won't be tracing through the ceiling above.
///
public virtual TraceResult TraceBBox( Vector3 start, Vector3 end, Vector3 mins, Vector3 maxs, float liftFeet = 0.0f )
{
if ( liftFeet > 0 )
{
liftFeet *= Entity.Scale;
start += Vector3.Up * liftFeet;
maxs = maxs.WithZ( maxs.z - liftFeet );
}
var tr = Trace.Ray( start + TraceOffset, end + TraceOffset )
.Size( mins, maxs )
.WithAnyTags( "solid", "playerclip", "passbullets", "player" )
.Ignore( Entity )
.Run();
tr.EndPosition -= TraceOffset;
return tr;
}
///
/// Try to keep a walking player on the ground when running down slopes etc
///
public virtual void StayOnGround()
{
var start = Entity.Position + Vector3.Up * (2 * Entity.Scale);
var end = Entity.Position + Vector3.Down * (StepSize * Entity.Scale);
// See how far up we can go without getting stuck
var trace = TraceBBox( Entity.Position, start );
start = trace.EndPosition;
// Now trace down from a known safe position
trace = TraceBBox( start, end );
if ( trace.Fraction <= 0 ) return;
if ( trace.Fraction >= 1 ) return;
if ( trace.StartedSolid ) return;
if ( Vector3.GetAngle( Vector3.Up, trace.Normal ) > GroundAngle ) return;
// This is incredibly hacky. The real problem is that trace returning that strange value we can't network over.
// float flDelta = fabs( mv->GetAbsOrigin().z - trace.m_vEndPos.z );
// if ( flDelta > 0.5f * DIST_EPSILON )
Entity.Position = trace.EndPosition;
}
public Transform? GroundTransform;
public Sandbox.Entity? OldGroundEntity;
void RestoreGroundPos()
{
if ( Entity.GroundEntity == null || Entity.GroundEntity.IsWorld || GroundTransform == null || Entity.GroundEntity != OldGroundEntity )
return;
var worldTrns = Entity.GroundEntity.Transform.ToWorld( GroundTransform.Value );
if ( Prediction.FirstTime )
{
Entity.BaseVelocity = ((Entity.Position - worldTrns.Position) * -1) / Time.Delta;
}
//Entity.Position = (Entity.Position.WithZ( worldTrns.Position.z ));
}
void SaveGroundPos()
{
var ply = Entity as Player;
if ( Entity.GroundEntity == null || Entity.GroundEntity.IsWorld )
{
OldGroundEntity = null;
GroundTransform = null;
return;
}
if ( Entity.GroundEntity == OldGroundEntity )
{
GroundTransform = Entity.GroundEntity.Transform.ToLocal( Entity.Transform );
}
else
{
GroundTransform = null;
GroundTransformViewAngles = null;
}
OldGroundEntity = Entity.GroundEntity;
}
public Transform? GroundTransformViewAngles;
public Angles? PreviousViewAngles;
void RestoreGroundAngles()
{
if ( Entity.GroundEntity == null || Entity.GroundEntity.IsWorld || GroundTransformViewAngles == null || PreviousViewAngles == null )
return;
var ply = Entity as Player;
var worldTrnsView = Entity.GroundEntity.Transform.ToWorld( GroundTransformViewAngles.Value );
ply.ViewAngles -= (PreviousViewAngles.Value.ToRotation() * worldTrnsView.Rotation.Inverse).Angles().WithPitch( 0 ).WithRoll( 0 );
}
void SaveGroundAngles()
{
if ( Entity.GroundEntity == null || Entity.GroundEntity.IsWorld )
{
GroundTransformViewAngles = null;
return;
}
var ply = Entity as Player;
GroundTransformViewAngles = Entity.GroundEntity.Transform.ToLocal( new Transform( Vector3.Zero, ply.ViewAngles.ToRotation() ) );
PreviousViewAngles = ply.ViewAngles;
}
bool PushDebug = false;
[SkipHotload] Dictionary OldTransforms;
Transform OldTransform;
void DoPushingStuff()
{
var tr = TraceBBox( Entity.Position, Entity.Position );
if ( tr.StartedSolid
&& tr.Entity != null
&& tr.Entity != OldGroundEntity
&& tr.Entity != Entity.GroundEntity
&& !tr.Entity.IsWorld
&& OldTransforms != null
&& OldTransforms.TryGetValue( tr.Entity.NetworkIdent, out var oldTransform ) )
{
if ( tr.Entity is BasePhysics ) return;
var oldPosition = Entity.Position;
var oldTransformLocal = oldTransform.ToLocal( Entity.Transform );
var newTransform = tr.Entity.Transform.ToWorld( oldTransformLocal );
// this used to be just the direction of the tr delta however pushing outwards a llittle seems more appropriate
var direction = ((Entity.Position - newTransform.Position) * -1);
direction += (Entity.Position - tr.Entity.Position).Normal.WithZ( 0 ) * 0.8f;
FindIdealMovementDirection( newTransform.Position, direction, out var outOffset, out var outDirection );
var newPosition = newTransform.Position + (outDirection * outOffset) + (outDirection * 0.1f);
// Check if we're being crushed, if not we set our position.
if ( IsBeingCrushed( newPosition ) )
{
OnCrushed( tr.Entity );
}
else
{
Entity.Velocity += (outDirection / Time.Delta);
// insurance we dont instantly get stuck again add a little extra.
Entity.Position = newPosition;
}
}
// In order to get the delta of transforms we have not yet touched, we grab the transforms of EVERYTHING with in a radius.
// The radius we look in is determined by player speed and velocity
GetPossibleTransforms();
}
void FindIdealMovementDirection( Vector3 Position, Vector3 Direction, out float OutOffset, out Vector3 OutDirection )
{
OutDirection = Direction;
OutOffset = 0;
// ------------------------ shit ------------------------
// look into doing this nicer at somepoint
// ------------------------------------------------------
// brute force our way into finding how much extra we need to be pushed in the case of AABB edges being still inside of the object
for ( int i = 0; i < 512; i++ )
{
var possibleoffset = (float)(i) / 16f;
var pos = Position + (Direction * possibleoffset);
var offsettr = TraceBBox( pos, pos );
if ( !offsettr.StartedSolid )
{
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Green, 5 );
OutDirection = Direction;
OutOffset = possibleoffset;
break;
}
//sidewards test, for things moving sideways and upwards or downwards
var posside = Position + (Direction.WithZ( 0 ) * possibleoffset);
var offsettrside = TraceBBox( posside, posside );
if ( !offsettrside.StartedSolid )
{
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Green, 5 );
OutDirection = Direction.WithZ( 0 );
OutOffset = possibleoffset;
break;
}
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Red, 5 );
}
// ------------------------------------------------------
}
bool IsBeingCrushed( Vector3 NewPosition )
{
//do a trace that will decide whether or not we're being crushed
var crushtrace = Trace.Ray( Entity.Position, NewPosition )
.Ignore( Entity )
.Run();
if ( PushDebug ) DebugOverlay.Line( crushtrace.StartPosition, crushtrace.EndPosition, Color.Blue, 5 );
return crushtrace.Fraction != 1;
}
void OnCrushed( Entity CurshingEntity )
{
// deal crush damage!
if ( !Game.IsServer ) return;
//if ( CurshingEntity is DoorEntity || CurshingEntity is PlatformEntity || CurshingEntity is PathPlatformEntity )
//{
// Entity.TakeDamage( DamageInfo.Generic( 5 ).WithTag( "crush" ) );
//}
// if we get crushed by a door, change its direction.
if ( CurshingEntity is DoorEntity door )
{
if ( door.State == DoorEntity.DoorState.Opening )
{
door.Close();
door.Close();
}
else if ( door.State == DoorEntity.DoorState.Closing )
{
door.Open();
door.Open();
}
}
}
/*void DoPushingStuff()
{
var tr = TraceBBox( Entity.Position, Entity.Position );
if ( tr.StartedSolid && tr.Entity != null && !tr.Entity.IsWorld && tr.Entity != OldGroundEntity && tr.Entity != Entity.GroundEntity )
{
var tf = new Transform();
if ( OldTransforms != null && OldTransforms.TryGetValue( tr.Entity.NetworkIdent, out tf ) )
{
var x = tf.ToLocal( Entity.Transform );
var x2 = tr.Entity.Transform.ToWorld( x );
// grab the delta between the last ticks position and this ticks position
var x3 = ((Entity.Position - x2.Position) * -1);//.WithZ( 0 );
var x4 = ((Entity.Position - x2.Position) * -1).WithZ( 0 );
// grab the delta between the last ticks position and this ticks position
var oldpos = Entity.Position;
var oldvel = Entity.Velocity;
bool unstuck = false;
for ( int i = 0; i < 2048; i++ )
{
var ch = (x3 * (i / 16.0f));
var pos = Entity.Position + ch;
tr.Entity.ResetInterpolation();
Entity.ResetInterpolation();
var tr2 = TraceBBox( pos, pos );
if ( !tr2.StartedSolid )
{
var x5 = ((oldpos - pos) * -1);
var tr3 = Trace.Ray( Entity.Position, pos + x3 ).Ignore( tr.Entity ).Ignore( Entity ).Run();
if ( tr3.Fraction != 1 )
{
var ch2 = (x4 * (i / 16.0f));
var pos2 = Entity.Position + ch2;
var tr4 = TraceBBox( pos2, pos2 );
var tr5 = Trace.Ray( Entity.Position, pos2 + x4 ).Ignore( tr.Entity ).Ignore( Entity ).Run();
if ( !tr4.StartedSolid && tr5.Fraction == 1 )
{
x3 = x4;
ch = ch2;
pos = pos2;
}
else
{
//crush / stuck
if ( tr.Entity is DoorEntity door )
{
if ( Game.IsServer )
{
if ( door.State == DoorEntity.DoorState.Opening )
door.Close();
if ( door.State == DoorEntity.DoorState.Closing )
door.Open();
Entity.TakeDamage( DamageInfo.Generic( 10 ).WithTag( "crush" ) );
}
GetPossibleTransforms();
OldTransforms.Remove( tr.Entity.NetworkIdent );
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Red, 5 );
return;
}
else
{
if ( Game.IsServer )
{
Entity.TakeDamage( DamageInfo.Generic( 10 ).WithTag( "crush" ) );
}
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Red, 5 );
continue;
}
}
}
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Green, 5 );
if ( !(x3 / Time.Delta).AlmostEqual( Vector3.Zero, 0.01f ) )
{
Entity.Velocity += (x3 / Time.Delta);
Entity.Position = pos + x3;
}
//Entity.Velocity = (x5 / Time.Delta);
unstuck = true;
break;
}
else
{
if ( PushDebug ) DebugOverlay.Line( Entity.Position, pos, Color.Red, 5 );
}
}
if ( unstuck )
{
if ( PushDebug ) DebugOverlay.Text( "unstuck", Entity.Position, Color.Green );
}
else
{
if ( PushDebug ) DebugOverlay.Text( "failed", Entity.Position, Color.Red );
}
}
}
GetPossibleTransforms();
}*/
void GetPossibleTransforms()
{
var a = Sandbox.Entity.FindInSphere( Entity.Position, 512 + Entity.Velocity.Length );
var b = new Dictionary();
foreach ( var i in a )
{
b.Add( i.NetworkIdent, i.Transform );
}
OldTransforms = b;
OldTransform = Entity.Transform;
}
}