using Godot; using Movementtests.player_controller.Scripts; namespace Movementtests.systems; public partial class MoveSystem : Node3D { 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 ); [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; private const float DecelerationSpeedFactorFloor = 5.0f; private const float DecelerationSpeedFactorAir = 1.0f; public float CrouchTransitionSpeed { get; set; } = 20.0f; [Export(PropertyHint.Range, "0,5,0.1,or_greater")] public float DoubleJumpSpeedFactor { get; set; } = 2f; public bool CanDoubleJump { get; set; } = true; private float _lastFrameWasOnFloor = -Mathf.Inf; 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; } public void MoveAround(MoveAroundParameters param) { var (delta, movementDirection, isOnFloor, isDead, isHeadTouchingCeiling, isHanging) = param; var doesCapsuleHaveCrouchingHeight = _capsuleCollider.IsCrouchingHeight(); var doesCapsuleHaveDefaultHeight = _capsuleCollider.IsDefaultHeight(); if (isHanging) { _parent.Velocity = Vector3.Zero; _parent.MoveAndSlide(); return; } // Adding the gravity if (!isOnFloor) { _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: _parent.Velocity.Y - (_gravity.CalculateGravityForce() * (float)delta), z: _parent.Velocity.Z); } if (isOnFloor) { _lastFrameWasOnFloor = Engine.GetPhysicsFrames(); CanDoubleJump = true; } // 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; } if (isOnFloor) { // Set velocity based on input direction when on the floor if (direction.Length() > 0) { float newX = direction.X * _currentSpeed; float newZ = direction.Z * _currentSpeed; _parent.Velocity = new Vector3(newX, _parent.Velocity.Y, newZ); } // If there is no input, smoothly decelerate the character on the floor else { float xDeceleration = Mathf.Lerp(_parent.Velocity.X, direction.X * _currentSpeed, (float)delta * DecelerationSpeedFactorFloor); float zDeceleration = Mathf.Lerp(_parent.Velocity.Z, direction.Z * _currentSpeed, (float)delta * DecelerationSpeedFactorFloor); _parent.Velocity = new Vector3(xDeceleration, _parent.Velocity.Y, zDeceleration); } } else { float xDeceleration = Mathf.Lerp(_parent.Velocity.X, direction.X * _currentSpeed, (float)delta * DecelerationSpeedFactorAir); float zDeceleration = Mathf.Lerp(_parent.Velocity.Z, direction.Z * _currentSpeed, (float)delta * DecelerationSpeedFactorAir); _parent.Velocity = new Vector3(xDeceleration, _parent.Velocity.Y, zDeceleration); } if (isDead) { _parent.MoveAndSlide(); } } public void Jump(bool isOnFloor) { var mantleLocationResult = _mantleSystem.FindMantleInFrontOfPlayer(); if (mantleLocationResult.IsSome(out var mantleLocation)) { var duration = 0.1f * mantleLocation.DistanceTo(_parent.Position); _tweenQueueSystem.QueueTween(mantleLocation, duration); } else if (isOnFloor) { _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: _gravity.CalculateJumpForce(), z: _parent.Velocity.Z); } else if (CanDoubleJump) { CanDoubleJump = false; _parent.Velocity = new Vector3( x: _parent.Velocity.X, y: _gravity.CalculateJumpForce() * DoubleJumpSpeedFactor, z: _parent.Velocity.Z); } } }