using Godot; using Movementtests.player_controller.Scripts; using RustyOptions; namespace Movementtests.systems; public partial class MoveSystem : Node3D { public enum JumpTypes { SimpleJump, DoubleJump, JumpFromDash, JumpFromWall } public record MoveSystemParameters( CharacterBody3D Parent, Gravity Gravity, MantleSystem MantleSystem, TweenQueueSystem TweenQueueSystem, HeadSystem HeadSystem, CapsuleCollider CapsuleCollider); public record MoveAroundParameters( double Delta, Vector3 MovementDirection, bool IsOnFloor, bool IsDead, bool IsHeadTouchingCeiling, bool isHanging, bool isWallHugging ); [Export(PropertyHint.Range, "0,20,0.1,or_greater")] public float WalkSpeed { get; set; } = 5.0f; [Export(PropertyHint.Range, "0,20,0.1,or_greater")] public float SprintSpeed { get; set; } = 7.2f; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float CrouchSpeed { get; set; } = 2.5f; [Export(PropertyHint.Range, "0,100,0.1,or_greater")] public float _currentSpeed; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float AccelerationSpeedFactorFloor = 5.0f; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float DecelerationSpeedFactorFloor = 5.0f; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float DecelerationSpeedFactorAir = 1.0f; [Export(PropertyHint.Range, "0,1,0.01,or_greater")] public float ApexHoldTime = 0.0f; private float _timeLeftAtApex = 0.0f; private bool _wasGoingUpLastFrame = false; public float CrouchTransitionSpeed { get; set; } = 20.0f; [Export(PropertyHint.Range, "0,5,0.1,or_greater")] public float WallHugGravityReducingFactor { get; set; } = 0.1f; private Gravity _gravity; private CharacterBody3D _parent; private MantleSystem _mantleSystem; private TweenQueueSystem _tweenQueueSystem; private CapsuleCollider _capsuleCollider; private HeadSystem _headSystem; public void Init(MoveSystemParameters parameters) { _parent = parameters.Parent; _gravity = parameters.Gravity; _mantleSystem = parameters.MantleSystem; _tweenQueueSystem = parameters.TweenQueueSystem; _capsuleCollider = parameters.CapsuleCollider; _headSystem = parameters.HeadSystem; _currentSpeed = WalkSpeed; } private bool IsGoingUp() { return _parent.Velocity.Y > 0; } public void MoveAround(MoveAroundParameters param) { var (delta, movementDirection, isOnFloor, isDead, isHeadTouchingCeiling, isHanging, isWallHugging) = param; var doesCapsuleHaveCrouchingHeight = _capsuleCollider.IsCrouchingHeight(); var doesCapsuleHaveDefaultHeight = _capsuleCollider.IsDefaultHeight(); if (IsGoingUp() || isOnFloor) { _wasGoingUpLastFrame = true; _timeLeftAtApex = ApexHoldTime; } if (isHanging) { _parent.Velocity = Vector3.Zero; _parent.MoveAndSlide(); return; } if (isWallHugging) { _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: _parent.Velocity.Y - _gravity.CalculateGravityForce() * (float)delta * WallHugGravityReducingFactor, z: _parent.Velocity.Z); return; } // Adding the gravity if (!isOnFloor) { if (!IsGoingUp() && _wasGoingUpLastFrame && _timeLeftAtApex > 0) { _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: 0, z: _parent.Velocity.Z); _timeLeftAtApex -= (float) delta; } else { _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: _parent.Velocity.Y - (_gravity.CalculateGravityForce() * (float)delta), z: _parent.Velocity.Z); } } // The code below is required to quickly adjust player's position on Y-axis when there's a ceiling on the // trajectory of player's jump and player is standing if (isHeadTouchingCeiling && doesCapsuleHaveDefaultHeight) { _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: _parent.Velocity.Y - 2.0f, z: _parent.Velocity.Z); } if (!isDead) { // Used both for detecting the moment when we enter into crouching mode and the moment when we're already // in the crouching mode if (Input.IsActionPressed("crouch") || (doesCapsuleHaveCrouchingHeight && isHeadTouchingCeiling)) { _capsuleCollider.Crouch((float)delta, CrouchTransitionSpeed); _currentSpeed = CrouchSpeed; } // Used both for the moment when we exit the crouching mode and for the moment when we just walk else { _capsuleCollider.UndoCrouching((float)delta, CrouchTransitionSpeed); _currentSpeed = WalkSpeed; } } // Each component of the boolean statement for sprinting is required if (Input.IsActionPressed("sprint") && !isHeadTouchingCeiling && !doesCapsuleHaveCrouchingHeight && !isDead) { _currentSpeed = SprintSpeed; } // Basis is a 3x4 matrix. It contains information about scaling and rotation of head. // By multiplying our Vector3 by this matrix we're doing multiple things: // a) We start to operate in global space; // b) We're applying to Vector3 the current rotation of "head" object; // c) We're applying to Vector3 the current scaling of "head" object; Vector3 direction = _headSystem.Transform.Basis * movementDirection; if (isDead) { direction = Vector3.Zero; } var accelerationFloorFactor = direction.Length() > 0 ? AccelerationSpeedFactorFloor : DecelerationSpeedFactorFloor; var accelerationFactor = isOnFloor ? accelerationFloorFactor : DecelerationSpeedFactorAir; float xAcceleration = Mathf.Lerp(_parent.Velocity.X, direction.X * _currentSpeed, (float)delta * accelerationFactor); float zAcceleration = Mathf.Lerp(_parent.Velocity.Z, direction.Z * _currentSpeed, (float)delta * accelerationFactor); _parent.Velocity = new Vector3(xAcceleration, _parent.Velocity.Y, zAcceleration); if (isDead) { _parent.MoveAndSlide(); } } public void Jump(JumpTypes jumpType, Vector3? jumpDirection = null, float boost = 1.0f) { var effectiveJumpDirection = jumpDirection ?? Vector3.Up; var jumpForce = 0.0f; switch (jumpType) { case JumpTypes.DoubleJump: jumpForce = _gravity.CalculateDoubleJumpForce(); break; case JumpTypes.SimpleJump: jumpForce = _gravity.CalculateJumpForce(); break; case JumpTypes.JumpFromDash: jumpForce = _gravity.CalculateJumpFromDashForce(); break; case JumpTypes.JumpFromWall: jumpForce = _gravity.CalculateJumpFromWallForce(); break; default: jumpForce = _gravity.CalculateJumpForce(); break; } var currentHorizontalVelocity = new Vector3(_parent.Velocity.X, 0, _parent.Velocity.Z); var jumpVelocity = jumpForce * effectiveJumpDirection * boost; _parent.Velocity = currentHorizontalVelocity + jumpVelocity; } public bool CanMantle() { var mantleLocationResult = _mantleSystem.FindMantleInFrontOfPlayer(); return mantleLocationResult.IsSome(out _); } public Option Mantle() { var mantleLocationResult = _mantleSystem.FindMantleInFrontOfPlayer(); if (mantleLocationResult.IsSome(out var mantleLocation)) { var duration = 0.1f * mantleLocation.DistanceTo(_parent.Position); var tween = _tweenQueueSystem.TweenToLocation(new TweenQueueSystem.TweenInputs(mantleLocation, duration)); return tween.Some(); } return Option.None; } }