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; } }