using System; using Godot; using GodotStateCharts; using Movementtests.systems; using Movementtests.player_controller.Scripts; using RustyOptions; public partial class PlayerController : CharacterBody3D { // User API to important child nodes. public HeadSystem HeadSystem; public Bobbing Bobbing; public FieldOfView FieldOfView; public Stamina Stamina; public StairsSystem StairsSystem; public MantleSystem MantleSystem; public DashSystem DashSystem; public CapsuleCollider CapsuleCollider; public Gravity Gravity; public HealthSystem HealthSystem; public MoveSystem MoveSystem; public TweenQueueSystem TweenQueueSystem; public Node3D WeaponRoot; public WeaponSystem WeaponSystem; public WallHugSystem WallHugSystem; public PlayerUi PlayerUi; public TextureRect DashIndicator; private bool _movementEnabled = true; private bool _shouldMantle; private Vector3 _mantleLocation = Vector3.Zero; private Vector3 _dashDirection = Vector3.Zero; private float _lastFrameWasOnFloor = -Mathf.Inf; private const int NUM_OF_HEAD_COLLISION_DETECTORS = 4; private RayCast3D[] _headCollisionDetectors; private Vector3 _inputMove = Vector3.Zero; private float _inputRotateY; private float _inputRotateFloorplane; // Timers private Timer _coyoteTimer; private Timer _timeScaleAimInAirTimer; private Timer _timeAfterDashingTimer; private Timer _dashCooldownTimer; [Export(PropertyHint.Range, "0,1,0.01,or_greater")] public float TimeScaleAimInAir { get; set; } = 0.2f; [Export(PropertyHint.Range, "0,5,0.1,or_greater")] public float MaxJumpBoostAfterDashing { get; set; } = 1f; [Export(PropertyHint.Range, "0,5,1,or_greater")] public int MaxNumberOfDashActions { get; set; } = 1; [Export(PropertyHint.Range, "0,200,1,or_greater")] public int DashIndicatorStartSize { get; set; } = 100; [Export(PropertyHint.Range, "0,1,0.01")] public float DashProgressAfterWhichToAct { get; set; } = 0.8f; [Export] public Curve DashTimeDilationCurve { get; set; } private int _dashActionsLeft; public int DashActionsLeft { get => _dashActionsLeft; set { _dashActionsLeft = value; PlayerUi.SetNumberOfDashesLeft(value); } } private bool _isWallJumpAvailable = true; private StateChart _playerState; // Actions state private StateChartState _weaponInHand; private StateChartState _aiming; private StateChartState _dashing; private StateChartState _weaponThrown; private StateChartState _actionHanging; // Movement state private StateChartState _grounded; private StateChartState _crouched; private StateChartState _standing; private StateChartState _mantling; private StateChartState _movHanging; private StateChartState _wallHugging; private StateChartState _airborne; private StateChartState _coyoteEnabled; private StateChartState _jump; private StateChartState _jumpFromWall; private StateChartState _doubleJumpEnabled; private StateChartState _doubleJump; private StateChartState _falling; public override void _Ready() { /////////////////////////// // Getting components ///// /////////////////////////// // General use stuff TweenQueueSystem = GetNode("TweenQueueSystem"); PlayerUi = GetNode("UI"); DashIndicator = GetNode("%DashIndicator"); // Node3D mapNode = GetTree().Root.FindChild("Map", true, false) as Node3D; // Camera stuff HeadSystem = GetNode("HeadSystem"); Bobbing = GetNode("Bobbing"); FieldOfView = GetNode("FieldOfView"); Camera3D camera = GetNode("HeadSystem/CameraSmooth/Camera3D"); Node3D cameraSmooth = GetNode("HeadSystem/CameraSmooth"); ColorRect vignetteRect = GetNode( "HeadSystem/CameraSmooth/Camera3D/CLVignette(Layer_1)/HealthVignetteRect"); ColorRect distortionRect = GetNode( "HeadSystem/CameraSmooth/Camera3D/CLDistortion(Layer_2)/HealthDistortionRect"); ColorRect blurRect = GetNode("HeadSystem/CameraSmooth/Camera3D/CLBlur(Layer_2)/BlurRect"); // Movement stuff WeaponRoot = GetNode("WeaponRoot"); WeaponSystem = GetNode("WeaponRoot/WeaponSystem"); MantleSystem = GetNode("MantleSystem"); CapsuleCollider = GetNode("CapsuleCollider"); Gravity = GetNode("Gravity"); MoveSystem = GetNode("MoveSystem"); DashSystem = GetNode("DashSystem"); StairsSystem = GetNode("StairsSystem"); WallHugSystem = GetNode("WallHugSystem"); RayCast3D stairsBelowRayCast3D = GetNode("StairsBelowRayCast3D"); RayCast3D stairsAheadRayCast3D = GetNode("StairsAheadRayCast3D"); _headCollisionDetectors = new RayCast3D[NUM_OF_HEAD_COLLISION_DETECTORS]; for (int i = 0; i < NUM_OF_HEAD_COLLISION_DETECTORS; i++) { _headCollisionDetectors[i] = GetNode( "HeadCollisionDetectors/HeadCollisionDetector" + i); } // RPG Stuff Stamina = GetNode("Stamina"); HealthSystem = GetNode("HealthSystem"); // State management _playerState = StateChart.Of(GetNode("StateChart")); // Actions states _weaponInHand = StateChartState.Of(GetNode("StateChart/Root/Actions/WeaponInHand")); _aiming = StateChartState.Of(GetNode("StateChart/Root/Actions/Aiming")); _dashing = StateChartState.Of(GetNode("StateChart/Root/Actions/Dashing")); _weaponThrown = StateChartState.Of(GetNode("StateChart/Root/Actions/WeaponThrown")); _actionHanging = StateChartState.Of(GetNode("StateChart/Root/Actions/Hanging")); // Movement states _grounded = StateChartState.Of(GetNode("StateChart/Root/Movement/Grounded")); _standing = StateChartState.Of(GetNode("StateChart/Root/Movement/Grounded/Standing")); _crouched = StateChartState.Of(GetNode("StateChart/Root/Movement/Grounded/Crouched")); _mantling = StateChartState.Of(GetNode("StateChart/Root/Movement/Mantling")); _movHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/Hanging")); _airborne = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne")); _wallHugging = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/WallHugging")); _coyoteEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/CoyoteEnabled")); _jump = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/Jump")); _jumpFromWall = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/JumpFromWall")); _doubleJumpEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJumpEnabled")); _doubleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJump")); _falling = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/Falling")); // State timers _coyoteTimer = GetNode("CoyoteTime"); _dashCooldownTimer = GetNode("DashCooldown"); _timeScaleAimInAirTimer = GetNode("TimeScaleAimInAir"); _timeAfterDashingTimer = GetNode("TimeAfterDashing"); /////////////////////////// // Initialize components // /////////////////////////// // General use stuff TweenQueueSystem.Init(this); // Camera stuff HeadSystem.Init(); Bobbing.Init(camera); FieldOfView.Init(camera); // Movement stuff // Getting universal setting from GODOT editor to be in sync float gravitySetting = (float)ProjectSettings.GetSetting("physics/3d/default_gravity"); Gravity.Init(gravitySetting); MantleSystem.Init(HeadSystem); var moveSystemParams = new MoveSystem.MoveSystemParameters(this, Gravity, MantleSystem, TweenQueueSystem, HeadSystem, CapsuleCollider); MoveSystem.Init(moveSystemParams); StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth); DashSystem.Init(HeadSystem, camera, TweenQueueSystem); WeaponSystem.Init(HeadSystem, camera); WallHugSystem.Init(); // RPG Stuff HealthSystem.HealthSystemInitParams healthSystemParams = new HealthSystem.HealthSystemInitParams() { Gravity = Gravity, Parent = this, Camera = camera, Head = HeadSystem, VignetteRect = vignetteRect, DistortionRect = distortionRect, BlurRect = blurRect, }; HealthSystem.Init(healthSystemParams); Stamina.SetSpeeds(MoveSystem.WalkSpeed, MoveSystem.SprintSpeed); DashActionsLeft = MaxNumberOfDashActions; /////////////////////////// // Signal setup /////////// /////////////////////////// DashSystem.DashEnded += OnDashEnded; DashSystem.DashProgress += OnDashProgress; _weaponInHand.StateProcessing += HandleWeaponInHand; _aiming.StateProcessing += HandleAiming; _aiming.StateEntered += OnAimingEntered; _aiming.StateExited += ResetTimeScale; /*_crouched.StatePhysicsProcessing += HandleGroundedCrouched; _standing.StatePhysicsProcessing += HandleGroundedStanding;*/ _grounded.StateEntered += OnGrounded; _grounded.StatePhysicsProcessing += HandleGrounded; _airborne.StatePhysicsProcessing += HandleAirborne; _wallHugging.StatePhysicsProcessing += HandleWallHugging; _coyoteEnabled.StateEntered += StartCoyoteTime; _coyoteTimer.Timeout += CoyoteExpired; _timeScaleAimInAirTimer.Timeout += ResetTimeScale; _jump.StateEntered += Jump; _jumpFromWall.StateEntered += JumpFromWall; _doubleJump.StateEntered += DoubleJump; _mantling.StateEntered += Mantle; _dashing.StateEntered += OnDashStarted; _weaponThrown.StateEntered += OnWeaponThrown; } /////////////////////////// // Input Management /////// /////////////////////////// public void OnInputMove(Vector3 value) { _inputMove = value; } public void OnInputRotateY(float value) { _inputRotateY = value; } public void OnInputRotateFloorplane(float value) { _inputRotateFloorplane = value; } public void OnInputAimPressed() { _playerState.SendEvent("aim_pressed"); } public void OnInputAimDown() { _playerState.SendEvent("aim_down"); } public void OnInputAimReleased() { _playerState.SendEvent("aim_released"); } public void OnInputAimCanceled() { _playerState.SendEvent("aim_canceled"); DashSystem.CancelDash(); } public void OnInputHitPressed() { _playerState.SendEvent("hit_pressed"); } public void OnInputJumpPressed() { if (MoveSystem.CanMantle()) { _playerState.SendEvent("mantle"); return; } _playerState.SendEvent("jump"); } public void OnInputDropPressed() { _playerState.SendEvent("crouch"); } /////////////////////////// // Stateful logic ///////// /////////////////////////// // Simple states public void OnGrounded() { DashActionsLeft = MaxNumberOfDashActions; _isWallJumpAvailable = true; } public bool CanPerformDashAction() { return DashActionsLeft > 0 && _dashCooldownTimer.IsStopped(); } public void PerformDashAction() { _isWallJumpAvailable = true; _dashCooldownTimer.Start(); DashActionsLeft--; } // Jumping public void StartCoyoteTime() { _coyoteTimer.Start(); } public void CoyoteExpired() { _playerState.SendEvent("coyote_expired"); } public void Jump() { if (_aiming.Active && CanPerformDashAction()) { _playerState.SendEvent("jump_from_dash"); PerformDashAction(); PerformJump(MoveSystem.JumpTypes.JumpFromDash); return; } _playerState.SendEvent("to_double_jump"); PerformJump(MoveSystem.JumpTypes.SimpleJump); } public void JumpFromWall() { if (!_isWallJumpAvailable) return; _isWallJumpAvailable = false; var wallNormal = WallHugSystem.GetWallNormal().UnwrapOr(Vector3.Up); var isLookingTowardsWall = HeadSystem.GetForwardHorizontalVector().Dot(wallNormal) > 0.7; var jumpDirection = isLookingTowardsWall ? Vector3.Up : wallNormal; if (_aiming.Active && CanPerformDashAction()) { _playerState.SendEvent("jump_from_dash"); PerformDashAction(); PerformJump(MoveSystem.JumpTypes.JumpFromDash, jumpDirection); return; } _playerState.SendEvent("jump_from_wall"); PerformJump(MoveSystem.JumpTypes.JumpFromWall, jumpDirection); } public void DoubleJump() { _playerState.SendEvent("to_falling"); if (_aiming.Active && CanPerformDashAction()) { PerformDashAction(); PerformJump(MoveSystem.JumpTypes.JumpFromDash); return; } PerformJump(MoveSystem.JumpTypes.DoubleJump); } private void PerformJump(MoveSystem.JumpTypes jumpType, Vector3? jumpDirection = null) { var effectiveJumpDirection = jumpDirection ?? Vector3.Up; var jumpVector = (effectiveJumpDirection.Normalized() + Vector3.Up).Normalized(); var proportionOfTimeGone = _timeAfterDashingTimer.TimeLeft / _timeAfterDashingTimer.WaitTime; var actualBoost = 1 + MaxJumpBoostAfterDashing * proportionOfTimeGone; var makeItDouble = actualBoost > 1; if (makeItDouble && jumpType == MoveSystem.JumpTypes.SimpleJump) jumpType = MoveSystem.JumpTypes.DoubleJump; // convert simple jump to double if done right after a dash _timeAfterDashingTimer.Stop(); bool doesCapsuleHaveCrouchingHeight = CapsuleCollider.IsCrouchingHeight(); bool isPlayerDead = HealthSystem.IsDead(); if (!doesCapsuleHaveCrouchingHeight && !isPlayerDead) MoveSystem.Jump(jumpType, jumpVector, (float) actualBoost); } // Mantling public void Mantle() { var optionTween = MoveSystem.Mantle(); if (optionTween.IsSome(out var tween)) tween.Finished += MantleFinished; } public void MantleFinished() { _playerState.SendEvent("to_grounded"); } // Dashing and weapon throwing public void OnDashStarted() { if (!CanPerformDashAction()) { _playerState.SendEvent("aim_canceled"); _playerState.SendEvent("dash_ended"); DashSystem.CancelDash(); return; } PerformDashAction(); _timeAfterDashingTimer.Start(); if (WeaponSystem.FlyingState.Active) { DashSystem.ShouldMantle = false; DashSystem.PlannedPlayerLocation = WeaponSystem.GlobalPosition; } else if (WeaponSystem.PlantedState.Active) { DashSystem.ShouldMantle = false; var dashLocation = WeaponSystem.PlantLocation; if (WeaponSystem.IsPlantedInWall()) { dashLocation += WeaponSystem.PlantNormal * 0.5f; // Player radius } if (WeaponSystem.IsPlantedUnderPlatform()) { dashLocation += Vector3.Down * 1f; // Player height } DashSystem.PlannedPlayerLocation = dashLocation; } _dashDirection = (DashSystem.PlannedPlayerLocation - GlobalPosition).Normalized(); DashSystem.Dash(); } public void OnDashProgress(float progress) { Engine.SetTimeScale(DashTimeDilationCurve.Sample(progress)); DashIndicator.SetCustomMinimumSize(Vector2.One * DashIndicatorStartSize * (1 - progress)); var indicatorColor = progress < DashProgressAfterWhichToAct ? new Color(1, 1, 1) : new Color(0, 1, 0); DashIndicator.SetModulate(indicatorColor); } public void OnDashEnded() { // _playerState.SendEvent("enable_double_jump"); // Allow for double jump after dash -- OP ? // Regular dash if (WeaponSystem.InHandState.Active) { _playerState.SendEvent("dash_ended"); return; } // Store the weapon state before resetting it var isPlantedOnWall = WeaponSystem.IsPlantedInWall(); var isPlantedUnderPlatform = WeaponSystem.IsPlantedUnderPlatform(); var shouldDashToHanging = isPlantedOnWall || isPlantedUnderPlatform; var isFlying = WeaponSystem.FlyingState.Active; // Get the weapon back GetTree().GetRoot().RemoveChild(WeaponRoot); AddChild(WeaponRoot); WeaponRoot.SetGlobalPosition(GlobalPosition); WeaponSystem.ResetWeapon(); if (isFlying) { var vel = _dashDirection * DashSystem.PostDashSpeed; SetVelocity(vel); _playerState.SendEvent("dash_ended"); return; // In case states aren't exclusives } if (shouldDashToHanging) { _playerState.SendEvent("dash_to_planted"); return; // In case states aren't exclusives } // Weapon planted anywhere else _playerState.SendEvent("dash_ended"); } public void OnWeaponThrown() { RemoveChild(WeaponRoot); GetTree().GetRoot().AddChild(WeaponRoot); WeaponRoot.SetGlobalPosition(GlobalPosition); DashSystem.CancelDash(); var weaponTargetLocation = DashSystem.HasHit ? DashSystem.CollisionPoint : DashSystem.TargetLocation; WeaponSystem.ThrowWeapon( weaponTargetLocation, DashSystem.HasHit, DashSystem.CollisionPoint, DashSystem.CollisionNormal); } public void OnAimingEntered() { if (!isOnFloorCustom() && CanPerformDashAction()) ReduceTimeScaleWhileAiming(); } // Regular processes public void HandleWeaponInHand(float delta) { RotateWeaponWithPlayer(); } public void HandleAiming(float delta) { RotateWeaponWithPlayer(); if (isOnFloorCustom()) ResetTimeScale(); if (CanPerformDashAction()) DashSystem.PrepareDash(); else { _playerState.SendEvent("aim_canceled"); DashSystem.CancelDash(); } } // Physics processes public void HandleGrounded(float delta) { if (!isOnFloorCustom()) _playerState.SendEvent("start_falling"); } public void HandleGroundedStanding(float delta) { CapsuleCollider.UndoCrouching(delta, 1); HeadSystem.SetHeight(CapsuleCollider.GetCurrentHeight()); } public void HandleGroundedCrouched(float delta) { CapsuleCollider.Crouch(delta, 1); HeadSystem.SetHeight(CapsuleCollider.GetCurrentHeight()); } public void HandleAirborne(float delta) { if (isOnFloorCustom()) _playerState.SendEvent("grounded"); if (WallHugSystem.IsWallHugging() && Velocity.Y < 0) _playerState.SendEvent("wall_hug"); } public void HandleWallHugging(float delta) { if (!WallHugSystem.IsWallHugging()) _playerState.SendEvent("start_falling"); } /////////////////////////// // Stateless logic //////// /////////////////////////// private void LookAround() { Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane); HeadSystem.LookAround(inputLookDir); } private void MoveAround(double delta) { var moveAroundParams = new MoveSystem.MoveAroundParameters( delta, _inputMove, isOnFloorCustom(), HealthSystem.IsDead(), IsHeadTouchingCeiling(), _actionHanging.Active, _wallHugging.Active); MoveSystem.MoveAround(moveAroundParams); } private void HandleStairs(float delta) { StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams { IsOnFloorCustom = isOnFloorCustom(), IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(), CurrentSpeedGreaterThanWalkSpeed = MoveSystem._currentSpeed > MoveSystem.WalkSpeed, IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(), Delta = (float)delta, FloorMaxAngle = FloorMaxAngle, GlobalPositionFromDriver = GlobalPosition, Velocity = Velocity, GlobalTransformFromDriver = GlobalTransform, Rid = GetRid() }; StairsSystem.UpStairsCheckResult upStairsCheckResult = StairsSystem.SnapUpStairsCheck(upStairsCheckParams); if (upStairsCheckResult.UpdateRequired) { upStairsCheckResult.Update(this); } else { MoveAndSlide(); StairsSystem.DownStairsCheckParams downStairsCheckParams = new StairsSystem.DownStairsCheckParams { IsOnFloor = IsOnFloor(), // TODO: replace on IsOnFloor Custom IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(), LastFrameWasOnFloor = _lastFrameWasOnFloor, CapsuleDefaultHeight = CapsuleCollider.GetDefaultHeight(), CurrentCapsuleHeight = CapsuleCollider.GetCurrentHeight(), FloorMaxAngle = FloorMaxAngle, VelocityY = Velocity.Y, GlobalTransformFromDriver = GlobalTransform, Rid = GetRid() }; StairsSystem.DownStairsCheckResult downStairsCheckResult = StairsSystem.SnapDownStairsCheck( downStairsCheckParams); if (downStairsCheckResult.UpdateIsRequired) { downStairsCheckResult.Update(this); } } StairsSystem.SlideCameraParams slideCameraParams = new StairsSystem.SlideCameraParams { IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(), CurrentSpeedGreaterThanWalkSpeed = MoveSystem._currentSpeed > MoveSystem.WalkSpeed, BetweenCrouchingAndNormalHeight = CapsuleCollider.IsBetweenCrouchingAndNormalHeight(), Delta = (float)delta }; StairsSystem.SlideCameraSmoothBackToOrigin(slideCameraParams); } private void CameraModifications(float delta) { Bobbing.CameraBobbingParams cameraBobbingParams = new Bobbing.CameraBobbingParams { Delta = delta, IsOnFloorCustom = isOnFloorCustom(), Velocity = Velocity }; Bobbing.PerformCameraBobbing(cameraBobbingParams); FieldOfView.FovParameters fovParams = new FieldOfView.FovParameters { IsCrouchingHeight = CapsuleCollider.IsCrouchingHeight(), Delta = (float)delta, SprintSpeed = MoveSystem.SprintSpeed, Velocity = Velocity }; FieldOfView.PerformFovAdjustment(fovParams); } /////////////////////////// // Helpers //////////////// /////////////////////////// public void ReduceTimeScaleWhileAiming() { Engine.SetTimeScale(TimeScaleAimInAir); _timeScaleAimInAirTimer.Start(); } public void ResetTimeScale() { Engine.SetTimeScale(1); } private bool IsHeadTouchingCeiling() { for (int i = 0; i < NUM_OF_HEAD_COLLISION_DETECTORS; i++) { if (_headCollisionDetectors[i].IsColliding()) { return true; } } return false; } private bool isOnFloorCustom() { return IsOnFloor() || StairsSystem.WasSnappedToStairsLastFrame(); } public void RotateWeaponWithPlayer() { WeaponRoot.SetRotation(HeadSystem.Rotation); } /////////////////////////// // Processes ////////////// /////////////////////////// public override void _PhysicsProcess(double delta) { TweenQueueSystem.ProcessTweens(); LookAround(); MoveAround(delta); CameraModifications((float) delta); HandleStairs((float) delta); } }