using System; using Godot; using GodotStateCharts; using Movementtests.addons.godot_state_charts.csharp; using Movementtests.systems; using Movementtests.player_controller.Scripts; using RustyOptions; public enum JumpTypes { SimpleJump, DoubleJump, JumpFromDash, JumpFromWall } 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 HealthSystem HealthSystem; public TweenQueueSystem TweenQueueSystem; public Node3D WeaponRoot; public WeaponSystem WeaponSystem; public WallHugSystem WallHugSystem; public PlayerUi PlayerUi; public TextureRect DashIndicator; public ColorRect PowerCooldownIndicator; 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; private int _framesSinceJumpAtApex = 0; // Timers private Timer _timeScaleAimInAirTimer; private Timer _simpleDashCooldownTimer; private Timer _powerCooldownTimer; [ExportCategory("Movement")] [ExportGroup("Ground")] [Export(PropertyHint.Range, "0,20,0.1,or_greater")] public float WalkSpeed { get; set; } = 7.0f; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float AccelerationFloor = 5.0f; [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float DecelerationFloor = 5.0f; [ExportGroup("Air")] [Export(PropertyHint.Range, "0,10,0.1,or_greater")] public float AccelerationAir = 3.0f; [Export(PropertyHint.Range, "0,10,0.01,or_greater")] public float DecelerationAir = 1.0f; [Export(PropertyHint.Range, "0,10,0.01,or_greater")] public float Weight { get; set; } = 3.0f; // Jump [ExportGroup("Jump")] [Export(PropertyHint.Range, "0,1,0.01")] public float CoyoteTime { get; set; } = 0.2f; // Simple jump [ExportSubgroup("Simple jump")] [Export(PropertyHint.Range, "0,100,1,or_greater")] public float SimpleJumpStartVelocity { get; set; } = 3.0f; [Export(PropertyHint.Range, "0,10,1,or_greater")] public int SimpleJumpHangTimeInFrames { get; set; } = 5; [Export(PropertyHint.Range, "1,10,0.1,or_greater")] public float SimpleJumpGravityLesseningFactor { get; set; } = 3f; // Double jump [ExportSubgroup("Double jump")] [Export(PropertyHint.Range, "0,100,1,or_greater")] public float DoubleJumpStartVelocity { get; set; } = 10.0f; [Export(PropertyHint.Range, "0,10,1,or_greater")] public int DoubleJumpHangTimeInFrames { get; set; } = 5; [Export(PropertyHint.Range, "1,10,0.1,or_greater")] public float DoubleJumpGravityLesseningFactor { get; set; } = 3f; // Mega jump [ExportSubgroup("Mega jump")] [Export(PropertyHint.Range, "0,100,1,or_greater")] public float MegaJumpStartVelocity { get; set; } = 10.0f; [Export(PropertyHint.Range, "0,10,1,or_greater")] public int MegaJumpHangTimeInFrames { get; set; } = 5; [Export(PropertyHint.Range, "1,10,0.1,or_greater")] public float MegaJumpGravityLesseningFactor { get; set; } = 3f; // Wall jump [ExportSubgroup("Wall jump")] [Export(PropertyHint.Range, "0,100,1,or_greater")] public float WallJumpStartVelocity { get; set; } = 10.0f; [Export(PropertyHint.Range, "0,100,1,or_greater")] public float WallMegajumpStartVelocity { get; set; } = 20.0f; // Dash [ExportGroup("Dash")] // Simple dash [ExportSubgroup("Simple")] [Export(PropertyHint.Range, "0,50,0.1")] public float SimpleDashStrength { get; set; } = 10f; // Powered Dash [ExportSubgroup("Powered")] [Export(PropertyHint.Range, "0,1,0.01,or_greater")] public float PoweredDashTime { get; set; } = 0.3f; [Export(PropertyHint.Range, "0,100,0.1")] public float PoweredDashStrength { get; set; } = 10f; // Wall hug [ExportGroup("Wall hug")] [Export(PropertyHint.Range, "0,50,0.1,or_greater")] public float WallHugGravityLesseningFactor { get; set; } = 2f; [Export(PropertyHint.Range, "0.1,50,0.1,or_greater")] public float WallHugDownwardMaxSpeed { get; set; } = 2f; [Export(PropertyHint.Range, "0.1,10,0.1,or_greater")] public float WallHugHorizontalDeceleration { get; set; } = 5f; private float _targetSpeed; private float _gravity; [ExportCategory("Other")] [Export(PropertyHint.Range, "0,1,0.01,or_greater")] public float TimeScaleAimInAir { get; set; } = 0.05f; [Export(PropertyHint.Range, "0,5,1,or_greater")] public int MaxNumberOfDashActions { get; set; } = 1; [Export] public Curve DashTimeDilationCurve { get; set; } private bool _canDash = true; private int _empoweredActionsLeft; public int EmpoweredActionsLeft { get => _empoweredActionsLeft; set { _empoweredActionsLeft = value; PlayerUi.SetNumberOfDashesLeft(value); } } private bool _isWallJumpAvailable = true; private bool _isActionPerfectlyTimed = false; private StateChart _playerState; private StateChartState _weaponInHand; private StateChartState _aiming; private StateChartState _weaponThrown; private StateChartState _actionHanging; private StateChartState _empowerOn; private StateChartState _empowerOff; private StateChartState _powerExpired; private StateChartState _powerRecharging; private StateChartState _powerFull; private StateChartState _grounded; private StateChartState _mantling; private StateChartState _movHanging; private StateChartState _airborne; private StateChartState _coyoteEnabled; private StateChartState _simpleJump; private StateChartState _doubleJump; private StateChartState _megaJump; private StateChartState _simpleDash; private StateChartState _poweredDash; private StateChartState _doubleJumpEnabled; private StateChartState _onWall; private StateChartState _onWallHugCanceled; private StateChartState _onWallHugging; private StateChartState _onWallHanging; private StateChartState _falling; private Transition _onJumpFromWall; private Transition _onMegajumpFromWall; public override void _Ready() { /////////////////////////// // Getting components ///// /////////////////////////// // General use stuff TweenQueueSystem = GetNode("TweenQueueSystem"); PlayerUi = GetNode("UI"); // DashIndicator = GetNode("%DashIndicator"); PowerCooldownIndicator = GetNode("%DashCooldownIndicator"); PowerCooldownIndicator.Visible = false; EmpoweredActionsLeft = MaxNumberOfDashActions; _targetSpeed = WalkSpeed; // 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"); 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")); _weaponInHand = StateChartState.Of(GetNode("StateChart/Root/WeaponState/InHand")); _weaponThrown = StateChartState.Of(GetNode("StateChart/Root/WeaponState/Flying")); _aiming = StateChartState.Of(GetNode("StateChart/Root/Aim/On")); _simpleDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/Dash")); _poweredDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/PoweredDash")); // _actionHanging = StateChartState.Of(GetNode("StateChart/Root/Actions/Hanging")); _empowerOn = StateChartState.Of(GetNode("StateChart/Root/Empower/On")); _empowerOff = StateChartState.Of(GetNode("StateChart/Root/Empower/Off")); _powerExpired = StateChartState.Of(GetNode("StateChart/Root/PowerReserve/Expired")); _powerRecharging = StateChartState.Of(GetNode("StateChart/Root/PowerReserve/AtLeastOneCharge")); _powerFull = StateChartState.Of(GetNode("StateChart/Root/PowerReserve/Full")); _grounded = StateChartState.Of(GetNode("StateChart/Root/Movement/Grounded")); _mantling = StateChartState.Of(GetNode("StateChart/Root/Movement/Mantling")); _movHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hanging")); _airborne = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne")); _coyoteEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/CoyoteEnabled")); _simpleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/SimpleJump")); _doubleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/DoubleJump")); _megaJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/MegaJump")); _doubleJumpEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJumpEnabled")); _falling = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/Falling")); _onWall = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall")); _onJumpFromWall = Transition.Of(GetNode("StateChart/Root/Movement/OnWall/OnJump")); _onMegajumpFromWall = Transition.Of(GetNode("StateChart/Root/Movement/OnWall/OnMegajump")); _onWallHugging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hugging")); _onWallHugCanceled = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/HugCanceled")); _onWallHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hanging")); // State timers _powerCooldownTimer = GetNode("PowerCooldown"); _timeScaleAimInAirTimer = GetNode("TimeScaleAimInAir"); _simpleDashCooldownTimer = GetNode("DashCooldown"); /////////////////////////// // 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 _gravity = (float)ProjectSettings.GetSetting("physics/3d/default_gravity"); MantleSystem.Init(HeadSystem); StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth); DashSystem.Init(HeadSystem, camera, TweenQueueSystem); WeaponSystem.Init(HeadSystem, camera); WallHugSystem.Init(); // RPG Stuff HealthSystem.HealthSystemInitParams healthSystemParams = new HealthSystem.HealthSystemInitParams() { Parent = this, Camera = camera, Head = HeadSystem, VignetteRect = vignetteRect, DistortionRect = distortionRect, BlurRect = blurRect, }; HealthSystem.Init(healthSystemParams); Stamina.SetSpeeds(WalkSpeed, WalkSpeed); EmpoweredActionsLeft = MaxNumberOfDashActions; /////////////////////////// // Signal setup /////////// /////////////////////////// _weaponInHand.StateProcessing += HandleWeaponInHand; _aiming.StateProcessing += HandleAiming; _aiming.StateEntered += OnAimingEntered; _aiming.StateExited += ResetTimeScale; _aiming.StateExited += OnAimingExited; _grounded.StateEntered += OnGrounded; _grounded.StatePhysicsProcessing += HandleGrounded; _airborne.StateEntered += OnAirborne; _airborne.StatePhysicsProcessing += HandleAirborne; _coyoteEnabled.StateEntered += StartCoyoteTime; _timeScaleAimInAirTimer.Timeout += ResetTimeScale; // _weaponThrown.StateEntered += OnWeaponThrown; _powerFull.StateEntered += StopPowerCooldown; _powerFull.StateExited += StartPowerCooldown; _powerRecharging.StateEntered += StartPowerCooldown; _powerCooldownTimer.Timeout += PowerCooldownExpired; _powerRecharging.StateProcessing += PowerRecharging; _powerExpired.StateProcessing += PowerRecharging; _simpleJump.StateEntered += OnSimpleJumpStarted; _simpleJump.StatePhysicsProcessing += HandleSimpleJump; _doubleJump.StateEntered += OnDoubleJumpStarted; _doubleJump.StatePhysicsProcessing += HandleDoubleJump; _megaJump.StateEntered += OnMegaJumpStarted; _megaJump.StatePhysicsProcessing += HandleMegaJump; _simpleDash.StateEntered += OnSimpleDashStarted; _simpleDash.StatePhysicsProcessing += HandleSimpleDash; _poweredDash.StateEntered += OnPoweredDashStarted; _poweredDash.StatePhysicsProcessing += HandlePoweredDash; _poweredDash.StateExited += OnPoweredDashFinished; _simpleDashCooldownTimer.Timeout += DashCooldownTimeout; _onWallHugCanceled.StatePhysicsProcessing += HandleAirborne; _onWallHugging.StatePhysicsProcessing += HandleWallHugging; _onWallHanging.StatePhysicsProcessing += HandleWallHanging; _onJumpFromWall.Taken += OnJumpFromWall; _onMegajumpFromWall.Taken += OnMegajumpFromWall; } private bool _canDashAirborne = true; public void OnWallDetected() { FinishPoweredDash(); } public void OnGrounded() { _isWallJumpAvailable = true; _canDashAirborne = true; if (_simpleDashCooldownTimer.IsStopped()) _simpleDashCooldownTimer.Start(); } public void OnAirborne() { } public void DashCooldownTimeout() { _canDash = true; } public void HandleGrounded(float delta) { MoveOnGround(delta); if (!isOnFloorCustom()) _playerState.SendEvent("start_falling"); } public void HandleAirborne(float delta) { MoveInAir(delta); if (isOnFloorCustom()) _playerState.SendEvent("grounded"); if (WallHugSystem.IsWallHugging() && Velocity.Y < 0) _playerState.SendEvent("wall_hug"); } public void HandleWallHugging(float delta) { WallHug(delta); if (isOnFloorCustom()) _playerState.SendEvent("grounded"); if (!WallHugSystem.IsWallHugging()) _playerState.SendEvent("start_falling"); } public void HandleWallHanging(float delta) { WallHang(delta); } // Jump public void OnInputJumpStarted() { if (CanMantle()) { Mantle(); return; } // if (_grounded.Active || _coyoteEnabled.Active) // { // if (_empowerOn.Active && CanPerformEmpoweredAction()) // { // PerformEmpoweredAction(); // PerformJump(JumpTypes.JumpFromDash); // _playerState.SendEvent("megajump"); // } // } // else if (_doubleJumpEnabled.Active) // if (_empowerOn.Active && CanPerformEmpoweredAction()) // { // PerformEmpoweredAction(); // PerformJump(JumpTypes.JumpFromDash); // _playerState.SendEvent("megajump"); // } // else // PerformJump(JumpTypes.DoubleJump); // else if (_onWall.Active) // JumpFromWall(_empowerOn.Active); if (_empowerOn.Active && CanPerformEmpoweredAction()) { _playerState.SendEvent("megajump"); return; } _playerState.SendEvent("jump"); } public void OnInputJumpOngoing() { } public void OnInputJumpEnded() { _playerState.SendEvent("jump_ended"); } public void OnJumpStarted(float verticalVelocity) { _framesSinceJumpAtApex = 0; SetVerticalVelocity(verticalVelocity); } public void OnSimpleJumpStarted() { OnJumpStarted(SimpleJumpStartVelocity); } public void OnDoubleJumpStarted() { OnJumpStarted(DoubleJumpStartVelocity); } public void OnMegaJumpStarted() { PerformEmpoweredAction(); OnJumpStarted(MegaJumpStartVelocity); } public void WallHug(float delta) { var hvel = ComputeHVelocity(delta, WallHugHorizontalDeceleration, WallHugHorizontalDeceleration); var vvel = Velocity.Y - (CalculateGravityForce() * delta / WallHugGravityLesseningFactor); vvel = Math.Abs(vvel) > WallHugDownwardMaxSpeed ? -WallHugDownwardMaxSpeed : vvel; Velocity = hvel + vvel*Vector3.Up; } public void WallHang(float delta) { Velocity = Vector3.Zero; MoveAndSlide(); } public void ComputeJumpFromWallHSpeed(float jumpStrength) { // if (!_isWallJumpAvailable) // return; // _isWallJumpAvailable = false; // var isLookingTowardsWall = HeadSystem.GetForwardHorizontalVector().Dot(wallNormal) > 0.5; // var jumpDirection = isLookingTowardsWall ? Vector3.Up : wallNormal; var wallNormal = WallHugSystem.GetWallNormal().UnwrapOr(Vector3.Up); var jumpVector = wallNormal * jumpStrength; SetHorizontalVelocity(new Vector2(jumpVector.X, jumpVector.Z)); } public void OnJumpFromWall() { ComputeJumpFromWallHSpeed(WallJumpStartVelocity); } public void OnMegajumpFromWall() { ComputeJumpFromWallHSpeed(WallMegajumpStartVelocity); } public Vector3 ComputeHVelocity(float delta, float accelerationFactor, float decelerationFactor, Vector3? direction = null) { var dir = direction ?? HeadSystem.Transform.Basis * _inputMove; var acceleration = dir.Length() > 0 ? accelerationFactor : decelerationFactor; float xAcceleration = Mathf.Lerp(Velocity.X, dir.X * _targetSpeed, delta * acceleration); float zAcceleration = Mathf.Lerp(Velocity.Z, dir.Z * _targetSpeed, delta * acceleration); return new Vector3(xAcceleration, 0, zAcceleration); } public Vector3 ComputeHVelocityGround(float delta) { return ComputeHVelocity(delta, AccelerationFloor, DecelerationFloor); } public Vector3 ComputeHVelocityAir(float delta) { return ComputeHVelocity(delta, AccelerationAir, DecelerationAir); } public void SetVerticalVelocity(float verticalVelocity) { Velocity = new Vector3( x: Velocity.X, y: verticalVelocity, z: Velocity.Z); } public void SetHorizontalVelocity(Vector2 velocity) { Velocity = new Vector3( x: velocity.X, y: Velocity.Y, z: velocity.Y); } public void HandleJump(float delta, float gravityFactor, int hangFrames) { // Update horizontal velocity var horizontalVelocity = ComputeHVelocityAir(delta); Velocity = new Vector3(horizontalVelocity.X, Velocity.Y, horizontalVelocity.Z); // Hang time at the top of the jump if (Velocity.Y <= Mathf.Epsilon) { _framesSinceJumpAtApex++; SetVerticalVelocity(0); } // Cancel gravity on jump apex var gravity = CalculateGravityForce() / gravityFactor; var isAtApex = _framesSinceJumpAtApex > 0; if (isAtApex) { gravity = 0; } // Update velocity accordingly var newVerticalSpeed = Velocity.Y - gravity * delta; SetVerticalVelocity(newVerticalSpeed); if (IsHeadTouchingCeiling()) { SetVerticalVelocity(Velocity.Y - 2.0f); } // Move back to Airborne state management when starting to go down again if (_framesSinceJumpAtApex > hangFrames) _playerState.SendEvent("jump_ended"); } public void HandleSimpleJump(float delta) { HandleJump(delta, SimpleJumpGravityLesseningFactor, SimpleJumpHangTimeInFrames); } public void HandleDoubleJump(float delta) { HandleJump(delta, DoubleJumpGravityLesseningFactor, DoubleJumpHangTimeInFrames); } public void HandleMegaJump(float delta) { HandleJump(delta, MegaJumpGravityLesseningFactor, MegaJumpHangTimeInFrames); } public void PowerRecharging(float delta) { var progress = (float) (_powerCooldownTimer.TimeLeft / _powerCooldownTimer.WaitTime); PowerCooldownIndicator.SetSize(new Vector2(100 * progress, 10)); } public void StartPowerCooldown() { _powerCooldownTimer.Start(); PowerCooldownIndicator.Visible = true; } public void StopPowerCooldown() { _powerCooldownTimer.Stop(); PowerCooldownIndicator.Visible = false; } public void PowerCooldownExpired() { EmpoweredActionsLeft += 1; var eventToSend = EmpoweredActionsLeft == MaxNumberOfDashActions ? "fully_charged" : "recharge"; _playerState.SendEvent(eventToSend); } /////////////////////////// // 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"); if (!WeaponSystem.InHandState.Active) { OnDashStarted(); } } public void OnInputAimDown() { _playerState.SendEvent("aim_down"); } public void OnInputAimReleased() { _playerState.SendEvent("aim_released"); } public void OnInputAimCanceled() { _playerState.SendEvent("cancel"); DashSystem.CancelDash(); } public void OnInputHitPressed() { if (_aiming.Active) { OnWeaponThrown(); } } public void OnInputEmpowerDown() { _playerState.SendEvent("empower_down"); } public void OnInputEmpowerReleased() { _playerState.SendEvent("empower_released"); } private bool _dashAvailable = true; public void OnInputDashPressed() { if (_empowerOn.Active && CanPerformEmpoweredAction()) { PerformEmpoweredAction(); _playerState.SendEvent("powered_dash"); return; } if (_airborne.Active) { if (!_canDashAirborne) return; _canDashAirborne = false; } _playerState.SendEvent("dash"); } public void OnSimpleDashStarted() { if (!_canDash) return; _canDash = false; var dashStrength = SimpleDashStrength; var direction = GetInputGlobalHDirection(); SetVelocity(direction * dashStrength); } public void HandleSimpleDash(float delta) { _playerState.SendEvent("dash_finished"); } public Vector3 GetInputGlobalHDirection() { var direction = HeadSystem.Transform.Basis * _inputMove; return new Vector3(direction.X, 0, direction.Z).Normalized(); } public Vector3 GetInputLocalHDirection() { var direction = _inputMove; return new Vector3(direction.X, 0, direction.Z).Normalized(); } public void OnPoweredDashStarted() { Velocity = GetInputGlobalHDirection() * PoweredDashStrength; GetTree().CreateTimer(PoweredDashTime).Timeout += FinishPoweredDash; } public void OnPoweredDashFinished() { // Try mantling here } public void FinishPoweredDash() { _playerState.SendEvent("dash_finished"); } public void HandlePoweredDash(float delta) { var collision = MoveAndCollide(Velocity * delta, maxCollisions: 10); if (collision != null) { FinishPoweredDash(); } } public bool CanPerformEmpoweredAction() { return EmpoweredActionsLeft > 0; } public void PerformEmpoweredAction() { _isWallJumpAvailable = true; EmpoweredActionsLeft--; _playerState.SendEvent(EmpoweredActionsLeft <= 0 ? "expired" : "power_used"); } // Jumping public void StartCoyoteTime() { GetTree().CreateTimer(CoyoteTime).Timeout += CoyoteExpired; } public void CoyoteExpired() { _playerState.SendEvent("coyote_expired"); } public void JumpFromWall(bool isEmpowered) { if (!_isWallJumpAvailable) return; _isWallJumpAvailable = false; var wallNormal = WallHugSystem.GetWallNormal().UnwrapOr(Vector3.Up); var isLookingTowardsWall = HeadSystem.GetForwardHorizontalVector().Dot(wallNormal) > 0.5; var jumpDirection = isLookingTowardsWall ? Vector3.Up : wallNormal; if (isEmpowered && CanPerformEmpoweredAction()) { PerformEmpoweredAction(); PerformJump(JumpTypes.JumpFromDash, jumpDirection); return; } PerformJump(JumpTypes.JumpFromWall, jumpDirection); } private void PerformJump(JumpTypes jumpType, Vector3? jumpDirection = null) { var effectiveJumpDirection = jumpDirection ?? Vector3.Up; var jumpVector = (effectiveJumpDirection.Normalized() + Vector3.Up).Normalized(); bool doesCapsuleHaveCrouchingHeight = CapsuleCollider.IsCrouchingHeight(); bool isPlayerDead = HealthSystem.IsDead(); if (!doesCapsuleHaveCrouchingHeight && !isPlayerDead) Jump(jumpType, jumpVector); } public void Jump(JumpTypes jumpType, Vector3? jumpDirection = null, float boost = 1.0f) { var effectiveJumpDirection = jumpDirection ?? Vector3.Up; var jumpForce = 100.0f; var currentHorizontalVelocity = new Vector3(Velocity.X, 0, Velocity.Z); var jumpVelocity = jumpForce * effectiveJumpDirection * boost; Velocity = currentHorizontalVelocity + jumpVelocity; } // Mantling public void Mantle() { _playerState.SendEvent("mantle"); var optionTween = FindMantle(); if (optionTween.IsSome(out var tween)) tween.Finished += MantleFinished; } public bool CanMantle() { var mantleLocationResult = MantleSystem.FindMantleInFrontOfPlayer(); return mantleLocationResult.IsSome(out _); } public Option FindMantle() { var mantleLocationResult = MantleSystem.FindMantleInFrontOfPlayer(); if (mantleLocationResult.IsSome(out var mantleLocation)) { var duration = 0.1f * mantleLocation.DistanceTo(Position); var tween = TweenQueueSystem.TweenToLocation(new TweenQueueSystem.TweenInputs(mantleLocation, duration)); return tween.Some(); } return Option.None; } public void MantleFinished() { _playerState.SendEvent("grounded"); } // Dashing and weapon throwing public void OnDashStarted() { if (!CanPerformEmpoweredAction()) { _playerState.SendEvent("aim_canceled"); _playerState.SendEvent("dash_ended"); DashSystem.CancelDash(); return; } PerformEmpoweredAction(); 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 OnDashEnded() { // 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() { DashSystem.CancelDash(); _playerState.SendEvent("cancel_aim"); RemoveChild(WeaponRoot); GetTree().GetRoot().AddChild(WeaponRoot); WeaponRoot.SetGlobalPosition(GlobalPosition); var weaponTargetLocation = DashSystem.HasHit ? DashSystem.CollisionPoint : DashSystem.TargetLocation; WeaponSystem.ThrowWeapon( weaponTargetLocation, DashSystem.HasHit, DashSystem.CollisionPoint, DashSystem.CollisionNormal); } public void OnAimingEntered() { if (!WeaponSystem.InHandState.Active) { OnDashStarted(); return; } if (!isOnFloorCustom() && CanPerformEmpoweredAction()) ReduceTimeScaleWhileAiming(); } public void OnAimingExited() { DashSystem.CancelDash(); } // Regular processes public void HandleWeaponInHand(float delta) { if (WeaponSystem.InHandState.Active) RotateWeaponWithPlayer(); } public void HandleAiming(float delta) { RotateWeaponWithPlayer(); if (CanPerformEmpoweredAction()) DashSystem.PrepareDash(); } /////////////////////////// // Stateless logic //////// /////////////////////////// private void LookAround() { Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane); HeadSystem.LookAround(inputLookDir); } public void MoveOnGround(double delta) { var horizontalVelocity = ComputeHVelocityGround((float) delta); Velocity = new Vector3(horizontalVelocity.X, Velocity.Y, horizontalVelocity.Z); } public void MoveInAir(double delta) { var horizontalVelocity = ComputeHVelocityAir((float) delta); var verticalVelocity = Velocity.Y - (CalculateGravityForce() * (float)delta); Velocity = new Vector3(horizontalVelocity.X, verticalVelocity, horizontalVelocity.Z); } private void HandleStairs(float delta) { StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams { IsOnFloorCustom = isOnFloorCustom(), IsCapsuleHeightLessThanNormal = CapsuleCollider.IsCapsuleHeightLessThanNormal(), CurrentSpeedGreaterThanWalkSpeed = false, 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(), 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 = false, BetweenCrouchingAndNormalHeight = CapsuleCollider.IsBetweenCrouchingAndNormalHeight(), Delta = 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 = delta, SprintSpeed = WalkSpeed, Velocity = Velocity }; FieldOfView.PerformFovAdjustment(fovParams); } /////////////////////////// // Helpers //////////////// /////////////////////////// public float CalculateGravityForce() => _gravity * Weight; 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); } }