Files
MovementTests/player_controller/Scripts/PlayerController.cs
Minimata 7c74b8b5e5
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 19s
Create tag and build when new code gets to main / Export (push) Successful in 10m2s
removing broken ABC and refactoring enemy movement
2026-01-17 19:55:51 +01:00

1760 lines
55 KiB
C#

using System;
using Godot;
using GodotStateCharts;
using Movementtests.addons.godot_state_charts.csharp;
using Movementtests.interfaces;
using Movementtests.systems;
using Movementtests.player_controller.Scripts;
using RustyOptions;
public partial class PlayerController : CharacterBody3D, IDamageable, IKnockbackable, IDamageMaker
{
// Enums
public enum AllowedInputs
{
All,
MoveCamera,
None,
}
private bool _isUsingGamepad;
public AllowedInputs CurrentlyAllowedInputs { get; set; } = AllowedInputs.All;
public enum BufferedActions
{
None,
Jump,
MantleJump,
Dash,
MantleDash
}
private BufferedActions _bufferedAction = BufferedActions.None;
///////////////////////////
// Signals and events //
///////////////////////////
///////////////////////////
// Public stuff //
///////////////////////////
public HeadSystem HeadSystem;
public Bobbing Bobbing;
public FieldOfView FieldOfView;
public StairsSystem StairsSystem;
public MantleSystem MantleSystem;
public DashSystem DashSystem;
public CollisionShape3D StandingCollider;
public CollisionShape3D SlideCollider;
public Node3D WeaponRoot;
public WeaponSystem WeaponSystem;
public WallHugSystem WallHugSystem;
public PlayerUi PlayerUi;
public TextureRect DashIndicator;
public ColorRect PowerCooldownIndicator;
public Node3D DashIndicatorNode;
public MeshInstance3D DashIndicatorMesh;
public CylinderMesh DashIndicatorMeshCylinder;
public RayCast3D WallRunSnapper;
public ShapeCast3D GroundDetector;
public ShapeCast3D CeilingDetector;
public RayCast3D DirectGroundDetector;
public Area3D WeaponHitbox;
// Inspector stuff
[Export] public Marker3D TutorialWeaponTarget;
[Export] public bool TutorialDone { get; set; }
[ExportCategory("Combat")]
[ExportGroup("Damage")]
[Export]
public RDamage GetDamageDealt { get; set; }
[Export]
public CDamageable CDamage { get; set; }
[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;
// Mantle
[ExportGroup("Mantle")]
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float MantleTime { get; set; } = 0.1f;
[Export]
public PackedScene MantlePath { get; set; }
[Export(PropertyHint.Range, "0,50,0.1")]
public float MantleDashStrength { get; set; } = 15f;
[Export(PropertyHint.Range, "0,100,1,or_greater")]
public float MantleJumpStartVelocity { get; set; } = 20.0f;
// Jump
[ExportGroup("Jump")]
[Export(PropertyHint.Range, "0,1,0.01")]
public float CoyoteTime { get; set; } = 0.2f;
[Export(PropertyHint.Range, "0,10,1,or_greater")]
public int InputBufferFrames { get; set; } = 3;
// 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;
// Wall jump
[ExportSubgroup("Wall jump")]
[Export(PropertyHint.Range, "0,100,1,or_greater")]
public float WallJumpStartVelocity { get; set; } = 10.0f;
// Dash
[ExportGroup("Dash")]
[Export(PropertyHint.Range, "0,5,1,or_greater")]
public int MaxNumberOfEmpoweredActions { get; set; } = 1;
// Simple dash
[ExportSubgroup("Simple")]
[Export(PropertyHint.Range, "0,50,0.1,or_greater")]
public float SimpleDashStrength { get; set; } = 10f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float SimpleDashTime { get; set; } = 0.5f;
// Aimed Dash
[ExportSubgroup("Special")]
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float AimedDashTime { get; set; } = 0.1f;
[Export(PropertyHint.Range, "0,100,1,or_greater")]
public float PostDashSpeed { get; set; } = 100f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float TimeScaleAimInAir { get; set; } = 0.05f;
// Slam
[ExportSubgroup("Slam")]
[Export(PropertyHint.Range, "0,100,1,or_greater")]
public float SlamSpeed { get; set; } = 50.0f;
// Sliding and gliding
[ExportGroup("Slide")]
[ExportSubgroup("Ground slide")]
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float AccelerationGroundSlide = 1.0f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float DecelerationGroundSlide = 0.1f;
[Export(PropertyHint.Range, "0.99,1,0.0001")]
public float FlatGroundSlideSpeedLossRate = 0.9975f;
[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
public float GroundSlideJumpMultiplier = 1.0f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float GroundSlideJumpSpeedFactor;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float GroundSlideDownSlopeAcceleration = 0.1f;
[Export(PropertyHint.Range, "0,100,0.1,or_greater")]
public float GroundSlideDownSlopeMaxSpeed = 50f;
[Export(PropertyHint.Range, "1,10,0.1,or_greater")]
public float GroundSlideSlopeMagnetism = 2f;
[ExportSubgroup("Air glide")]
[Export]
public bool AllowForVelocityRedirection = true;
[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
public float AirGlideVSpeed { get; set; } = 1.0f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float AccelerationAirGlide = 1.0f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float DecelerationAirGlide = 0.1f;
[Export(PropertyHint.Range, "0,10,0.01,or_greater")]
public float AirGlideVerticalAcceleration = 1.0f;
[Export(PropertyHint.Range, "0,10,0.1,or_greater")]
public float AirGlideJumpMultiplier = 1.0f;
[Export(PropertyHint.Range, "0,1,0.01,or_greater")]
public float AirGlideJumpSpeedFactor;
// 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;
// Wall run
[ExportGroup("Wall run")]
[Export(PropertyHint.Range, "1,20,0.1,or_greater")]
public float WallRunUpwardVelocity { get; set; } = 10f;
[Export(PropertyHint.Range, "1,20,0.1,or_greater")]
public float WallRunAltitudeLossSpeed { get; set; } = 10f;
[Export(PropertyHint.Range, "1,20,0.1,or_greater")]
public float WallRunSpeedThreshold { get; set; } = 8f;
///////////////////////////
// Private stuff //
///////////////////////////
// Stairs and shit
private float _lastFrameWasOnFloor = -Mathf.Inf;
private const int NUM_OF_HEAD_COLLISION_DETECTORS = 4;
private RayCast3D[] _headCollisionDetectors;
// Basic movement
private bool _movementEnabled = true;
private Vector3 _inputMove = Vector3.Zero;
private Vector3 _inputMoveKeyboard = Vector3.Zero;
private float _inputRotateY;
private float _inputRotateFloorplane;
// Basic falling
private float _targetSpeed;
private float _gravity;
// Jump stuff
private int _currentInputBufferFrames;
private bool _isJumpInputPressed;
private int _framesSinceJumpAtApex;
private bool _isWallJumpAvailable = true;
// Mantle stuff
private bool _shouldMantleOnDashEnded;
private Path _mantlePath;
private bool _customMantle;
private Transform3D _customMantleStartTransform;
private Curve3D _customMantleCurve;
private Vector3 _mantleStartPosition;
private Vector3 _velocityOnMantleStarted = Vector3.Zero;
// Wall stuff
private Vector3 _wallHugStartLocation = Vector3.Zero;
private Vector3 _wallHugStartNormal = Vector3.Zero;
private Vector3 _wallHugStartProjectedVelocity = Vector3.Zero;
private Vector3 _currentWallContactPoint = Vector3.Zero;
// Dash stuff
private bool _canDash = true;
private bool _canDashAirborne = true;
private float _playerHeight;
private float _playerRadius;
private Vector3 _dashDirection = Vector3.Zero;
private Vector3 _preDashVelocity = Vector3.Zero;
private int _empoweredActionsLeft;
public int EmpoweredActionsLeft
{
get => _empoweredActionsLeft;
set
{
_empoweredActionsLeft = value;
PlayerUi.SetNumberOfDashesLeft(value);
}
}
// Settings
private float _lookSensitivityMultiplier = 1.0f;
private float _mouseSensitivityMultiplier = 1.0f;
private float _headBobbingMultiplier = 1.0f;
private float _fovChangeMultiplier = 1.0f;
// Timers
private Timer _timeScaleAimInAirTimer;
private Timer _simpleDashCooldownTimer;
private Timer _airborneDashCooldownTimer;
private Timer _powerCooldownTimer;
// State chart
private StateChart _playerState;
private StateChartState _aiming;
private StateChartState _powerExpired;
private StateChartState _powerRecharging;
private StateChartState _powerFull;
private StateChartState _grounded;
private StateChartState _airborne;
private StateChartState _coyoteEnabled;
private StateChartState _jumping;
private StateChartState _simpleJump;
private StateChartState _doubleJump;
private StateChartState _mantling;
private StateChartState _simpleDash;
private StateChartState _aimedDash;
private StateChartState _sliding;
private StateChartState _groundSliding;
private StateChartState _airGliding;
private StateChartState _airGlidingDoubleJump;
private StateChartState _slideCanceled;
private StateChartState _slamming;
private StateChartState _onWall;
private StateChartState _onWallHugging;
private StateChartState _onWallHanging;
private StateChartState _onWallRunning;
private Transition _onJumpFromWall;
private Transition _onJumpFromWallFalling;
private Transition _onLeaveWallFromRun;
private Transition _onGroundSlideJump;
private Transition _onAirGlideDoubleJump;
public override void _Ready()
{
LoadSettings();
///////////////////////////
// Getting components /////
///////////////////////////
// General use stuff
PlayerUi = GetNode<PlayerUi>("UI");
// DashIndicator = GetNode<TextureRect>("%DashIndicator");
PowerCooldownIndicator = GetNode<ColorRect>("%DashCooldownIndicator");
PowerCooldownIndicator.Visible = false;
EmpoweredActionsLeft = MaxNumberOfEmpoweredActions;
_targetSpeed = WalkSpeed;
DashIndicatorNode = GetNode<Node3D>("DashIndicator");
DashIndicatorMesh = GetNode<MeshInstance3D>("DashIndicator/DashIndicatorMesh");
DashIndicatorMeshCylinder = DashIndicatorMesh.Mesh as CylinderMesh;
DashIndicatorMesh.Visible = false;
// Camera stuff
HeadSystem = GetNode<HeadSystem>("HeadSystem");
Bobbing = GetNode<Bobbing>("Bobbing");
FieldOfView = GetNode<FieldOfView>("FieldOfView");
Camera3D camera = GetNode<Camera3D>("HeadSystem/CameraSmooth/Camera3D");
Node3D cameraSmooth = GetNode<Node3D>("HeadSystem/CameraSmooth");
// Movement stuff
WeaponRoot = GetNode<Node3D>("WeaponRoot");
WeaponSystem = GetNode<WeaponSystem>("WeaponRoot/WeaponSystem");
MantleSystem = GetNode<MantleSystem>("HeadSystem/MantleSystem");
StandingCollider = GetNode<CollisionShape3D>("StandingCollider");
SlideCollider = GetNode<CollisionShape3D>("SlideCollider");
DashSystem = GetNode<DashSystem>("DashSystem");
StairsSystem = GetNode<StairsSystem>("StairsSystem");
WallHugSystem = GetNode<WallHugSystem>("WallHugSystem");
WallRunSnapper = GetNode<RayCast3D>("%WallRunSnapper");
GroundDetector = GetNode<ShapeCast3D>("GroundDetector");
CeilingDetector = GetNode<ShapeCast3D>("CeilingDetector");
DirectGroundDetector = GetNode<RayCast3D>("DirectGroundDetector");
RayCast3D stairsBelowRayCast3D = GetNode<RayCast3D>("StairsBelowRayCast3D");
RayCast3D stairsAheadRayCast3D = GetNode<RayCast3D>("StairsAheadRayCast3D");
_headCollisionDetectors = new RayCast3D[NUM_OF_HEAD_COLLISION_DETECTORS];
for (int i = 0; i < NUM_OF_HEAD_COLLISION_DETECTORS; i++)
{
_headCollisionDetectors[i] = GetNode<RayCast3D>(
"HeadCollisionDetectors/HeadCollisionDetector" + i);
}
var playerShape = StandingCollider.GetShape() as CapsuleShape3D;
_playerHeight = playerShape!.Height;
_playerRadius = playerShape.Radius;
WeaponHitbox = GetNode<Area3D>("%WeaponHitbox");
// State management
_playerState = StateChart.Of(GetNode("StateChart"));
_aiming = StateChartState.Of(GetNode("StateChart/Root/Aim/On"));
_simpleDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/Dash"));
_aimedDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/AimedDash"));
_slamming = StateChartState.Of(GetNode("StateChart/Root/Movement/Slamming"));
_sliding = StateChartState.Of(GetNode("StateChart/Root/Movement/Sliding"));
_slideCanceled = StateChartState.Of(GetNode("StateChart/Root/Movement/Sliding/SlideCanceled"));
_groundSliding = StateChartState.Of(GetNode("StateChart/Root/Movement/Sliding/GroundSlide"));
_airGliding = StateChartState.Of(GetNode("StateChart/Root/Movement/Sliding/AirGlide"));
_airGlidingDoubleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Sliding/AirGlideDoubleJumpEnabled"));
_onGroundSlideJump = Transition.Of(GetNode("StateChart/Root/Movement/Sliding/GroundSlide/OnJump"));
_onAirGlideDoubleJump = Transition.Of(GetNode("StateChart/Root/Movement/Sliding/AirGlideDoubleJumpEnabled/OnJump"));
// _actionHanging = StateChartState.Of(GetNode("StateChart/Root/Actions/Hanging"));
_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"));
_airborne = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne"));
_coyoteEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/CoyoteEnabled"));
// _doubleJumpEnabled = StateChartState.Of(GetNode("StateChart/Root/Movement/Airborne/DoubleJumpEnabled"));
_jumping = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump"));
_simpleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/SimpleJump"));
_doubleJump = StateChartState.Of(GetNode("StateChart/Root/Movement/Jump/DoubleJump"));
_mantling = StateChartState.Of(GetNode("StateChart/Root/Movement/Mantling"));
_onJumpFromWall = Transition.Of(GetNode("StateChart/Root/Movement/OnWall/OnJump"));
_onJumpFromWallFalling = Transition.Of(GetNode("StateChart/Root/Movement/Airborne/Falling/OnWallJump"));
_onWall = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall"));
_onWallHugging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hugging"));
_onWallHanging = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Hanging"));
_onWallRunning = StateChartState.Of(GetNode("StateChart/Root/Movement/OnWall/Running"));
_onLeaveWallFromRun = Transition.Of(GetNode("StateChart/Root/Movement/OnWall/Running/OnLeaveWall"));
// State timers
_powerCooldownTimer = GetNode<Timer>("PowerCooldown");
_timeScaleAimInAirTimer = GetNode<Timer>("TimeScaleAimInAir");
_simpleDashCooldownTimer = GetNode<Timer>("DashCooldown");
_airborneDashCooldownTimer = GetNode<Timer>("AirborneDashCooldown");
///////////////////////////
// Initialize components //
///////////////////////////
// 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();
StairsSystem.Init(stairsBelowRayCast3D, stairsAheadRayCast3D, cameraSmooth);
DashSystem.Init(HeadSystem, camera);
WeaponSystem.Init(HeadSystem, camera);
WallHugSystem.Init();
EmpoweredActionsLeft = MaxNumberOfEmpoweredActions;
if (!TutorialDone)
PlaceWeaponForTutorial();
///////////////////////////
// Signal setup ///////////
///////////////////////////
_aiming.StatePhysicsProcessing += HandleAiming;
_aiming.StateEntered += OnAimingEntered;
_aiming.StateExited += ResetTimeScale;
_aiming.StateExited += OnAimingExited;
_grounded.StateEntered += OnGrounded;
_grounded.StatePhysicsProcessing += HandleGrounded;
_airborne.StatePhysicsProcessing += HandleAirborne;
_onWall.StatePhysicsProcessing += HandleOnWall;
_coyoteEnabled.StateEntered += StartCoyoteTime;
_timeScaleAimInAirTimer.Timeout += ResetTimeScale;
_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;
_mantling.StateEntered += OnMantleStarted;
_mantling.StatePhysicsProcessing += HandleMantling;
_simpleDash.StateEntered += OnSimpleDashStarted;
_simpleDash.StatePhysicsProcessing += HandleSimpleDash;
_aimedDash.StateEntered += OnAimedDashStarted;
_aimedDash.StateExited += OnAimedDashFinished;
_sliding.StateEntered += SlideStarted;
_sliding.StateExited += SlideEnded;
_slideCanceled.StateEntered += OnSlideCanceled;
_slideCanceled.StatePhysicsProcessing += HandleSlideCanceled;
_groundSliding.StatePhysicsProcessing += HandleGroundSlide;
_airGliding.StatePhysicsProcessing += HandleAirGlide;
_airGlidingDoubleJump.StatePhysicsProcessing += HandleAirGlide;
_onGroundSlideJump.Taken += JumpFromGroundSlide;
_onAirGlideDoubleJump.Taken += JumpFromAirGlide;
_slamming.StateEntered += SlamStarted;
_slamming.StateExited += SlamEnded;
_slamming.StatePhysicsProcessing += HandleSlam;
_simpleDashCooldownTimer.Timeout += DashCooldownTimeout;
_airborneDashCooldownTimer.Timeout += AirborneDashCooldownTimeout;
_onWall.StateEntered += OnWallStarted;
_onWall.StateExited += OnWallStopped;
_onWallHugging.StatePhysicsProcessing += HandleWallHugging;
_onWallHanging.StatePhysicsProcessing += HandleWallHanging;
_onWallRunning.StatePhysicsProcessing += HandleWallRunning;
_onJumpFromWall.Taken += OnJumpFromWall;
_onJumpFromWallFalling.Taken += OnJumpFromWall;
_onLeaveWallFromRun.Taken += OnLeaveWallFromRun;
}
///////////////////////////
// Settings & tutorial //
///////////////////////////
public void SetAllowedInputsAll()
{
CurrentlyAllowedInputs = AllowedInputs.All;
}
public void SetAllowedInputsMoveCamera()
{
CurrentlyAllowedInputs = AllowedInputs.MoveCamera;
}
public void SetAllowedInputsNone()
{
CurrentlyAllowedInputs = AllowedInputs.None;
}
public void LoadSettings()
{
var config = new ConfigFile();
// Load data from a file.
Error err = config.Load("user://config.cfg");
// If the file didn't load, ignore it.
if (err != Error.Ok)
{
throw new Exception("Couldn't load config.cfg");
}
_lookSensitivityMultiplier = (float) config.GetValue("InputSettings", "LookSensitivity", 1.0f);
_mouseSensitivityMultiplier = (float) config.GetValue("InputSettings", "MouseSensitivity", 1.0f);
_headBobbingMultiplier = (float) config.GetValue("InputSettings", "HeadBobbingWhileWalking", 1.0f);
_fovChangeMultiplier = (float) config.GetValue("InputSettings", "FovChangeWithSpeed", 1.0f);
}
public void OnTutorialDone(Node3D _)
{
TutorialDone = true;
}
public void PlaceWeaponForTutorial()
{
if (TutorialDone)
return;
RemoveChild(WeaponRoot);
GetTree().GetRoot().CallDeferred(Node.MethodName.AddChild, WeaponRoot);
WeaponRoot.CallDeferred(Node3D.MethodName.SetGlobalPosition, TutorialWeaponTarget.GlobalPosition);
WeaponSystem.CallDeferred(WeaponSystem.MethodName.PlaceWeaponForTutorial, TutorialWeaponTarget.GlobalPosition);
}
///////////////////////////
// Grounded management //
///////////////////////////
public void OnGrounded()
{
_isWallJumpAvailable = true;
_canDashAirborne = true;
if (_simpleDashCooldownTimer.IsStopped())
_simpleDashCooldownTimer.Start();
if (_bufferedAction == BufferedActions.MantleJump)
{
_playerState.SendEvent("jump");
}
if (_bufferedAction == BufferedActions.MantleDash)
{
if (GetMoveInput().Length() < Mathf.Epsilon)
{
_bufferedAction = BufferedActions.None;
return;
}
_playerState.SendEvent("dash");
}
if (_bufferedAction == BufferedActions.Jump && _currentInputBufferFrames > 0)
{
_currentInputBufferFrames = 0;
_playerState.SendEvent("jump");
}
if (_bufferedAction == BufferedActions.Dash && _currentInputBufferFrames > 0)
{
if (GetMoveInput().Length() < Mathf.Epsilon)
{
_bufferedAction = BufferedActions.None;
return;
}
_currentInputBufferFrames = 0;
_playerState.SendEvent("dash");
}
}
public bool IsGroundLike()
{
return GroundDetector.GetCollisionResult().Count > 0;
}
public void HandleGrounded(float delta)
{
MoveOnGround(delta);
// if (IsTryingToMantle()) _playerState.SendEvent("mantle");
if (!isOnFloorCustom())
_playerState.SendEvent("start_falling");
}
public void MoveOnGround(double delta)
{
var horizontalVelocity = ComputeHVelocityGround((float) delta);
Velocity = new Vector3(horizontalVelocity.X, Velocity.Y, horizontalVelocity.Z);
}
private void MoveSlideAndHandleStairs(float delta)
{
StairsSystem.UpStairsCheckParams upStairsCheckParams = new StairsSystem.UpStairsCheckParams
{
IsOnFloorCustom = isOnFloorCustom(),
IsCapsuleHeightLessThanNormal = false,
CurrentSpeedGreaterThanWalkSpeed = false,
IsCrouchingHeight = false,
Delta = delta,
FloorMaxAngle = FloorMaxAngle,
GlobalPositionFromDriver = GlobalPosition,
Velocity = Velocity,
GlobalTransformFromDriver = GlobalTransform,
Rid = GetRid()
};
StairsSystem.UpStairsCheckResult upStairsCheckResult = StairsSystem.SnapUpStairsCheck(upStairsCheckParams);
if (upStairsCheckResult.UpdateRequired && !_jumping.Active)
{
upStairsCheckResult.Update(this);
}
else
{
MoveAndSlide();
StairsSystem.DownStairsCheckParams downStairsCheckParams = new StairsSystem.DownStairsCheckParams
{
IsOnFloor = IsOnFloor(),
IsCrouchingHeight = false,
LastFrameWasOnFloor = _lastFrameWasOnFloor,
CapsuleDefaultHeight = _playerHeight,
CurrentCapsuleHeight = _playerHeight,
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 = false,
CurrentSpeedGreaterThanWalkSpeed = false,
BetweenCrouchingAndNormalHeight = false,
Delta = delta
};
StairsSystem.SlideCameraSmoothBackToOrigin(slideCameraParams);
}
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();
}
///////////////////////////
// Airborne management //
///////////////////////////
public void HandleAirborne(float delta)
{
MoveInAir(delta, IsGroundLike());
if (isOnFloorCustom())
_playerState.SendEvent("grounded");
if (IsTryingToMantle()) _playerState.SendEvent("mantle");
if (!WallHugSystem.IsWallHugging())
{
_isWallJumpAvailable = true; // reset wall jump if we left the wall
return;
}
// Going upwards, we stay simply airborne
if (Velocity.AngleTo(Vector3.Up) < Math.PI / 4)
return;
// Should we start a wall run
var wallNormal = WallHugSystem.WallHugNormal.UnwrapOr(Vector3.Zero);
var isIndeedWall = wallNormal.Y < 0.1;
var hvel = new Vector3(Velocity.X, 0, Velocity.Z);
var hvelProjected = hvel.Slide(_wallHugStartNormal);
var haveEnoughSpeed = hvelProjected.Length() > WallRunSpeedThreshold;
var isCoplanarEnough = Velocity.AngleTo(wallNormal) > Math.PI/4 && Velocity.AngleTo(wallNormal) < 3*Math.PI/4;
var isGoingDownwards = Velocity.AngleTo(Vector3.Down) < Math.PI/4;
if (haveEnoughSpeed && isCoplanarEnough && !isGoingDownwards && isIndeedWall && !_coyoteEnabled.Active)
{
SetVerticalVelocity(WallRunUpwardVelocity);
_playerState.SendEvent("wall_run");
return;
}
// If all else fail and we go down, we hug
if (Velocity.Y < 0 && IsInputTowardsWall(wallNormal))
{
_playerState.SendEvent("wall_hug");
}
}
public float ComputeVerticalSpeedGravity(float delta)
{
return Velocity.Y - CalculateGravityForce() * delta;
}
public void MoveInAir(double delta, bool isGroundLike = false)
{
var horizontalVelocity = isGroundLike ? ComputeHVelocityGround((float) delta) : ComputeHVelocityAir((float) delta);
var verticalVelocity = ComputeVerticalSpeedGravity((float) delta);
Velocity = new Vector3(horizontalVelocity.X, verticalVelocity, horizontalVelocity.Z);
}
///////////////////////////
// Movement input //
///////////////////////////
public bool IsPlayerInputtingForward()
{
return GetMoveInput().Z < -0.5f;
}
public void InputDeviceChanged(bool isUsingGamepad)
{
_isUsingGamepad = isUsingGamepad;
}
public Vector3 GetMoveInput()
{
if (_isUsingGamepad)
return _inputMove;
return _inputMoveKeyboard;
}
public Vector3 GetGlobalMoveInput()
{
return Transform.Basis * HeadSystem.Transform.Basis * GetMoveInput();
}
public Vector3 GetInputGlobalHDirection()
{
var direction = Transform.Basis * HeadSystem.Transform.Basis * GetMoveInput();
return new Vector3(direction.X, 0, direction.Z).Normalized();
}
public Vector3 GetInputLocalHDirection()
{
var direction = GetMoveInput();
return new Vector3(direction.X, 0, direction.Z).Normalized();
}
public void OnInputMoveKeyboard(Vector3 value)
{
_inputMoveKeyboard = value;
}
public void OnInputMove(Vector3 value)
{
_inputMove = value;
}
public void OnInputRotateY(float value)
{
_inputRotateY = value;
}
public void OnInputRotateFloorplane(float value)
{
_inputRotateFloorplane = value;
}
public bool IsTryingToMantle()
{
return MantleSystem.IsMantlePossible && IsPlayerInputtingForward() && _isJumpInputPressed;
}
///////////////////////////
// Utilities //
///////////////////////////
public float CalculateGravityForce() => _gravity * Weight;
// Camera stuff
private void LookAround(double delta)
{
Vector2 inputLookDir = new Vector2(_inputRotateY, _inputRotateFloorplane);
var lookSensitivity = _isUsingGamepad ? _lookSensitivityMultiplier : _mouseSensitivityMultiplier;
var wallHugContactPoint = _onWallRunning.Active ? _currentWallContactPoint : Vector3.Zero;
var playerVelocity = GetGlobalMoveInput();
HeadSystem.LookAround(delta, inputLookDir, playerVelocity, Velocity, wallHugContactPoint, lookSensitivity, isSliding: _groundSliding.Active);
}
public void RotateWeaponWithPlayer()
{
WeaponRoot.SetRotation(HeadSystem.Rotation);
}
public Vector3 GetGlobalForwardFacingVector()
{
return Transform.Basis * HeadSystem.Transform.Basis * Vector3.Forward;
}
private void CameraModifications(float delta)
{
Bobbing.CameraBobbingParams cameraBobbingParams = new Bobbing.CameraBobbingParams
{
Delta = delta,
IsOnFloorCustom = _grounded.Active || _onWallRunning.Active,
Velocity = Velocity,
SettingsMultiplier = _headBobbingMultiplier
};
Bobbing.PerformCameraBobbing(cameraBobbingParams);
FieldOfView.FovParameters fovParams = new FieldOfView.FovParameters
{
IsCrouchingHeight = false,
Delta = delta,
SprintSpeed = WalkSpeed,
Velocity = Velocity,
FOVMultiplier = _fovChangeMultiplier
};
FieldOfView.PerformFovAdjustment(fovParams);
}
// Horizontal velocity computing
public Vector3 ComputeHVelocity(float delta, float accelerationFactor, float decelerationFactor, Vector3? direction = null)
{
var dir = direction ?? GetGlobalMoveInput();
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);
}
// Velocity setters
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);
}
// Tweens
Tween CreatePositionTween(Vector3 targetLocation, float tweenTime)
{
var tween = GetTree().CreateTween();
tween.SetParallel();
tween.SetTrans(Tween.TransitionType.Cubic);
tween.SetEase(Tween.EaseType.InOut);
tween.TweenProperty(this, "global_position", targetLocation, tweenTime);
return tween;
}
// Child management
public void RemoveChildNode(Node3D node)
{
RemoveChild(node);
GetTree().GetRoot().AddChild(node);
node.SetGlobalPosition(GlobalPosition);
}
public void RecoverChildNode(Node3D node)
{
GetTree().GetRoot().RemoveChild(node);
AddChild(node);
node.SetGlobalPosition(GlobalPosition);
}
///////////////////////////
// Dash management //
///////////////////////////
public void DashCooldownTimeout()
{
_canDash = true;
}
public void AirborneDashCooldownTimeout()
{
_canDashAirborne = true;
}
public void OnInputDashPressed()
{
if (_aiming.Active && CanPerformEmpoweredAction())
{
PerformEmpoweredAction();
_playerState.SendEvent("aimed_dash");
_playerState.SendEvent("cancel_aim");
return;
}
if (GetMoveInput().Length() < Mathf.Epsilon) return;
// Buffer dash in case of mantle or inputting dash airborne before touching the ground without air dash available
_currentInputBufferFrames = InputBufferFrames;
_bufferedAction = _mantling.Active ? BufferedActions.MantleDash : BufferedActions.Dash;
if (_airborne.Active)
{
if (!_canDashAirborne)
return;
_canDashAirborne = false;
}
_playerState.SendEvent("dash");
}
public void SimpleDashInDirection(Vector3 direction, float strength = -1)
{
if (strength < 0) strength = SimpleDashStrength;
SetVelocity(direction * strength);
GetTree().CreateTimer(SimpleDashTime).Timeout += SimpleDashFinished;
}
public void SimpleDash(float strength = -1)
{
SimpleDashInDirection(GetInputGlobalHDirection(), strength);
}
public void OnSimpleDashStarted()
{
if (_bufferedAction == BufferedActions.MantleDash)
{
SimpleDash(MantleDashStrength);
_bufferedAction = BufferedActions.None;
return;
}
if (!_canDash)
{
var dashEvent = isOnFloorCustom() ? "grounded" : "dash_finished";
_playerState.SendEvent(dashEvent);
return;
}
_canDash = false;
SimpleDash();
_bufferedAction = BufferedActions.None;
}
public void HandleSimpleDash(float delta)
{
if (MantleSystem.IsMantlePossible && IsPlayerInputtingForward())
{
_bufferedAction = BufferedActions.MantleDash;
_playerState.SendEvent("mantle");
}
}
public void SimpleDashFinished()
{
var dashEvent = isOnFloorCustom() ? "grounded" : "dash_finished";
_playerState.SendEvent(dashEvent);
}
///////////////////////////
// On wall management //
///////////////////////////
public bool IsInputTowardsWall(Vector3 wallNormal)
{
return wallNormal.Dot(GetInputGlobalHDirection()) < -0.5;
}
public void HandleOnWall(float delta)
{
if (IsTryingToMantle()) _playerState.SendEvent("mantle");
}
public void OnWallDetected()
{
if (!_onWall.Active)
return;
var newWallNormal = WallHugSystem.WallHugNormal.UnwrapOr(Vector3.Up);
if (newWallNormal.AngleTo(_wallHugStartNormal) > Mathf.Pi/4) return;
_wallHugStartNormal = newWallNormal;
}
public void OnWallStarted()
{
if (!WallHugSystem.IsWallHugging())
return;
_wallHugStartNormal = WallHugSystem.WallHugNormal.UnwrapOr(Vector3.Up);
_currentWallContactPoint = WallHugSystem.WallHugLocation.UnwrapOr(Vector3.Zero);
_wallHugStartLocation = _currentWallContactPoint + _wallHugStartNormal * _playerRadius;
_wallHugStartProjectedVelocity = Velocity.Slide(_wallHugStartNormal);
}
public void OnWallStopped()
{
}
public void OnLeaveWallFromRun()
{
SimpleDashInDirection(Velocity.Normalized());
}
public void HandleWallHugging(float delta)
{
_canDash = true;
_canDashAirborne = true;
WallHug(delta);
if (isOnFloorCustom())
_playerState.SendEvent("grounded");
if (!WallHugSystem.IsWallHugging() || !IsInputTowardsWall(_wallHugStartNormal))
_playerState.SendEvent("start_falling");
}
public void HandleWallHanging(float delta)
{
WallHang(delta);
}
public void HandleWallRunning(float delta)
{
_canDash = false;
_canDashAirborne = false;
// Find horizontal velocity projected on the current wall
var hvel = new Vector3(Velocity.X, 0, Velocity.Z);
var hvelProjected = hvel.Slide(_wallHugStartNormal);
// Reorient horizontal velocity so we keep it coplanar with the wall without losing speed
var finalHVel = hvelProjected.Normalized() * hvel.Length();
// Adapt vertical speed
var verticalSpeed = Velocity.Y - WallRunAltitudeLossSpeed * delta;
Velocity = finalHVel + Vector3.Up*verticalSpeed;
Velocity *= 0.999f;
_currentWallContactPoint = WallHugSystem.WallHugLocation.UnwrapOr(Vector3.Zero);
if (isOnFloorCustom())
_playerState.SendEvent("grounded");
if (!WallHugSystem.IsWallHugging())
_playerState.SendEvent("start_falling");
}
public void WallHug(float delta)
{
var hvel = ComputeHVelocity(delta, WallHugHorizontalDeceleration, WallHugHorizontalDeceleration);
var hvelProjected = hvel.Slide(_wallHugStartNormal);
var vvel = Velocity.Y - (CalculateGravityForce() * delta / WallHugGravityLesseningFactor);
vvel = Math.Abs(vvel) > WallHugDownwardMaxSpeed ? -WallHugDownwardMaxSpeed : vvel;
Velocity = hvelProjected + vvel*Vector3.Up;
}
public void WallHang(float delta)
{
Velocity = Vector3.Zero;
GlobalPosition = _wallHugStartLocation;
}
public bool IsFacingWall()
{
return _wallHugStartNormal.Dot(GetGlobalForwardFacingVector()) < -0.5f;
}
///////////////////////////
// Jump management //
///////////////////////////
public void StartCoyoteTime()
{
GetTree().CreateTimer(CoyoteTime).Timeout += CoyoteExpired;
}
public void CoyoteExpired()
{
_playerState.SendEvent("coyote_expired");
}
public void OnInputJumpStarted()
{
_currentInputBufferFrames = InputBufferFrames;
if (_mantling.Active) _bufferedAction = BufferedActions.MantleJump;
// Don't overwrite mantle jump buffered action
else if (_bufferedAction == BufferedActions.None) _bufferedAction = BufferedActions.Jump;
_isJumpInputPressed = true;
PerformJump();
}
public void OnInputJumpOngoing()
{
}
public void OnInputJumpEnded()
{
_isJumpInputPressed = false;
_playerState.SendEvent("jump_ended");
}
public void PerformJump()
{
if (MantleSystem.IsMantlePossible && !_mantling.Active)
{
_playerState.SendEvent("mantle");
return;
}
if (WallHugSystem.IsWallHugging())
{
_playerState.SendEvent("wall_jump");
}
_playerState.SendEvent("jump");
}
private float _jumpStrengthMultiplier = 1.0f;
public void OnJumpStarted(float verticalVelocity)
{
_framesSinceJumpAtApex = 0;
var angle = GetFloorAngle();
var floorAngleFactor = angle > 1 ? 1 : 1 + angle;
SetVerticalVelocity(verticalVelocity*_jumpStrengthMultiplier*floorAngleFactor);
_jumpStrengthMultiplier = 1.0f;
}
public void OnSimpleJumpStarted()
{
if (_bufferedAction == BufferedActions.MantleJump)
{
SetVelocity(GetInputGlobalHDirection()*SimpleDashStrength);
OnJumpStarted(MantleJumpStartVelocity);
_bufferedAction = BufferedActions.None;
return;
}
OnJumpStarted(SimpleJumpStartVelocity);
_bufferedAction = BufferedActions.None;
}
public void OnDoubleJumpStarted()
{
_canDash = true;
// _canDashAirborne = true;
OnJumpStarted(DoubleJumpStartVelocity);
_bufferedAction = BufferedActions.None;
}
public void HandleJump(float delta, float gravityFactor, int hangFrames)
{
if (IsTryingToMantle()) _playerState.SendEvent("mantle");
// 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 when starting to go down again or if input isn't held anymore (buffered jump)
if (_framesSinceJumpAtApex > hangFrames || !_isJumpInputPressed)
_playerState.SendEvent("jump_ended");
}
public void HandleSimpleJump(float delta)
{
HandleJump(delta, SimpleJumpGravityLesseningFactor, SimpleJumpHangTimeInFrames);
}
public void HandleDoubleJump(float delta)
{
HandleJump(delta, DoubleJumpGravityLesseningFactor, DoubleJumpHangTimeInFrames);
}
// Jump and wall stuff
public void ComputeJumpFromWallHSpeed(float jumpStrength)
{
var wallNormal = WallHugSystem.WallHugNormal.UnwrapOr(Vector3.Up);
var jumpVector = wallNormal * jumpStrength;
var currentHorizontalVelocity = new Vector2(Velocity.X, Velocity.Z);
var wallJumpHorizontalVelocity = new Vector2(jumpVector.X, jumpVector.Z);
SetHorizontalVelocity(currentHorizontalVelocity + wallJumpHorizontalVelocity);
}
public void OnJumpFromWall()
{
if (!IsFacingWall() || (!_isWallJumpAvailable && IsFacingWall()))
{
ComputeJumpFromWallHSpeed(WallJumpStartVelocity);
}
// Remove the ability to dash straight away so you cannot scale up the wall
_canDashAirborne = false;
_airborneDashCooldownTimer.Start();
_isWallJumpAvailable = false;
}
///////////////////////////
// Mantle management //
///////////////////////////
public void OnMantleStarted()
{
HeadSystem.OnMantle();
_mantlePath = MantlePath.Instantiate() as Path;
if (_mantlePath == null)
{
GD.PrintErr("Failed to instantiate MantlePath");
return;
}
_velocityOnMantleStarted = Velocity;
var transform = _customMantle ? _customMantleStartTransform : MantleSystem.GlobalTransform;
var curve = _customMantle ? _customMantleCurve : MantleSystem.MantleCurve;
GetTree().GetRoot().AddChild(_mantlePath);
_mantlePath.Setup(transform, curve);
_mantleStartPosition = GlobalPosition;
var curveLength = curve.GetBakedLength();
var tween = GetTree().CreateTween();
tween.SetTrans(Tween.TransitionType.Linear);
tween.SetEase(Tween.EaseType.In);
tween.TweenProperty(_mantlePath.PathFollow, "progress_ratio", 1, MantleTime*curveLength);
tween.Finished += MantleFinished;
}
public void HandleMantling(float delta)
{
GlobalPosition = _mantlePath.Target.GlobalPosition;
}
public void MantleFinished()
{
_mantlePath.Teardown();
// SetVelocity(_finalCurveDirection.Normalized() * _speedOverCurve);
var isThereMovementInput = GetMoveInput().Length() > 0;
if (isThereMovementInput)
{
// If there's a movement input on Mantle, we dash in the direction the mantle ended with
var positionDifference = GlobalPosition - _mantleStartPosition;
var directionHorizontal = new Vector3(positionDifference.X, 0, positionDifference.Z);
// SimpleDashInDirection(directionHorizontal.Normalized());
SetVelocity(directionHorizontal.Normalized() * _velocityOnMantleStarted.Length());
}
_customMantle = false;
_playerState.SendEvent("grounded");
}
///////////////////////////
// Slide management //
///////////////////////////
private bool _isSlideInputDown = false;
public void OnInputSlideStarted()
{
_isSlideInputDown = true;
if (Velocity.Length() > WalkSpeed/2f)
_playerState.SendEvent("slide");
}
public void OnInputSlideEnded()
{
_isSlideInputDown = false;
if (_airGliding.Active || CanStandUpFromSlide())
_playerState.SendEvent("slide_released");
}
public record SlopeRecord(
Vector3 Position,
Vector3 Normal,
Vector3 Direction,
float AngleRadians
);
public Vector3 GetGroundPosition()
{
return DirectGroundDetector.GetCollisionPoint();
}
public Vector3 GetGroundNormal()
{
return DirectGroundDetector.GetCollisionNormal();
}
public SlopeRecord GetSlope()
{
var position = GetGroundPosition();
var normal = GetGroundNormal();
var angle = normal.AngleTo(Vector3.Up);
var vectorInPlane = normal.Cross(Vector3.Up).Normalized();
var direction = normal.Cross(vectorInPlane).Normalized();
return new SlopeRecord(position, normal, direction, angle);
}
public void SetupSlideCollision()
{
StandingCollider.Disabled = true;
SlideCollider.Disabled = false;
CeilingDetector.Enabled = true;
}
public void SetupStandingCollision()
{
StandingCollider.Disabled = false;
SlideCollider.Disabled = true;
CeilingDetector.Enabled = false;
}
public void SlideStarted()
{
_targetSpeed = Velocity.Length();
SetupSlideCollision();
}
public bool CanStandUpFromSlide()
{
return !CeilingDetector.IsColliding();
}
public bool CanCancelSlide()
{
return DirectGroundDetector.IsColliding()
&& Velocity.Length() < WalkSpeed / 2f
&& CanStandUpFromSlide();
}
public void SlideOnGround(float delta)
{
// Store current velocity
var currentVelocity = Velocity.Length();
// We prevent automatically losing speed when sliding under something
var speedLossRate = CanStandUpFromSlide() ? FlatGroundSlideSpeedLossRate : 1.0f;
// We force a minimum of speed when sliding under something
var minimumVelocity = CanStandUpFromSlide() ? 0f : WalkSpeed;
var finalSpeed = Mathf.Max(currentVelocity * speedLossRate, minimumVelocity);
// Going down a slope?
var (position, _, slopeDirection, slopeAngleRadians) = GetSlope();
// Change velocity based on Input
var horizontalVelocity = ComputeHVelocity(delta, AccelerationGroundSlide, DecelerationGroundSlide);
var newVelocityDirection = new Vector3(horizontalVelocity.X, Velocity.Y, horizontalVelocity.Z).Normalized();
var newVelocityHDirection = new Vector3(horizontalVelocity.X, 0, horizontalVelocity.Z).Normalized();
// var redirectedVelocity = newVelocityDirection.Slide(normal);
var redirectedVelocity = newVelocityDirection;
if (slopeAngleRadians > Mathf.Epsilon)
{
var slopeHDirection = new Vector3(slopeDirection.X, 0, slopeDirection.Z);
redirectedVelocity = newVelocityDirection.Lerp(slopeHDirection, delta * GroundSlideSlopeMagnetism);
var angleBetweenVelocityAndSlope = newVelocityHDirection.AngleTo(slopeHDirection);
var velocitySlopeAlignment = Mathf.Cos(angleBetweenVelocityAndSlope);
var slopeSpeedFactor = Mathf.Remap(velocitySlopeAlignment, -1, 1, 0.98, 1.02);
var speedFactorFromDownSlope = Velocity.Length() > GroundSlideDownSlopeMaxSpeed ? 1f : slopeSpeedFactor;
finalSpeed *= (float) speedFactorFromDownSlope;
// var redirectedVVelocity = slopeDirection.Y * velocitySlopeAlignment;
// redirectedVelocity = new Vector3(redirectedVelocity.X, redirectedVVelocity, redirectedVelocity.Z);
// Moving upslope and not enough speed
if (velocitySlopeAlignment < 0 && CanCancelSlide())
_playerState.SendEvent("slide_canceled");
}
else if (CanCancelSlide())
{
// Moving on flat ground and not enough speed
_playerState.SendEvent("slide_canceled");
}
// Preserve velocity when changing direction
var finalVelocity = redirectedVelocity.Normalized() * finalSpeed;
Velocity = finalVelocity;
if (DirectGroundDetector.IsColliding())
{
GlobalPosition = new Vector3(GlobalPosition.X, position.Y, GlobalPosition.Z);
}
}
public void OnSlideCanceled()
{
SetupStandingCollision();
_targetSpeed = WalkSpeed;
}
public void HandleSlideCanceled(float delta)
{
HandleGrounded(delta);
}
public void HandleGroundSlide(float delta)
{
SlideOnGround(delta);
if (MantleSystem.IsMantlePossible && IsPlayerInputtingForward()) _playerState.SendEvent("mantle");
if (!isOnFloorCustom() && !DirectGroundDetector.IsColliding()) _playerState.SendEvent("start_falling");
if (CanStandUpFromSlide() && !_isSlideInputDown) _playerState.SendEvent("slide_released");
}
public void GlideInAir(float delta)
{
if (AllowForVelocityRedirection)
{
// Preserve overall velocity
// Allows for tragic reorientation of the velocity vector after a fall
// Allows for bunny-hoping-like movement
var currentVelocity = Velocity.Length();
var horizontalVelocity = ComputeHVelocity(delta, AccelerationAirGlide, DecelerationAirGlide);
var verticalSpeed = Velocity.Y > 0 ? ComputeVerticalSpeedGravity(delta) : Mathf.Lerp(Velocity.Y, -AirGlideVSpeed, delta*AirGlideVerticalAcceleration);
var newVelocity = new Vector3(horizontalVelocity.X, verticalSpeed, horizontalVelocity.Z);
Velocity = newVelocity.Normalized() * currentVelocity;
}
else
{
// Preserve horizontal velocity only
// Allows for mor stable descent when gliding because you don't zoom away after a long fall
// Removes bunny-hoping-like movement by simply holding slide and jump jump jump
var currentHVelocity = new Vector2(Velocity.X, Velocity.Z).Length();
var horizontalVelocity = ComputeHVelocity(delta, AccelerationAirGlide, DecelerationAirGlide);
var newHVelocity = horizontalVelocity.Normalized() * currentHVelocity;
var verticalSpeed = Velocity.Y > 0 ? ComputeVerticalSpeedGravity(delta) : Mathf.Lerp(Velocity.Y, -AirGlideVSpeed, delta*AirGlideVerticalAcceleration);
var newVelocity = new Vector3(newHVelocity.X, verticalSpeed, newHVelocity.Z);
Velocity = newVelocity;
}
}
public void HandleAirGlide(float delta)
{
GlideInAir(delta);
if (MantleSystem.IsMantlePossible && IsPlayerInputtingForward()) _playerState.SendEvent("mantle");
if (isOnFloorCustom())
_playerState.SendEvent("grounded");
}
public void SlideEnded()
{
SetupStandingCollision();
_targetSpeed = WalkSpeed;
}
public void JumpFromGroundSlide()
{
_jumpStrengthMultiplier = GroundSlideJumpMultiplier + Velocity.Length()*GroundSlideJumpSpeedFactor;
}
public void JumpFromAirGlide()
{
_jumpStrengthMultiplier = AirGlideJumpMultiplier + Velocity.Length()*AirGlideJumpSpeedFactor;
}
///////////////////////////
// Slam Management ///////
///////////////////////////
public void OnInputSlamPressed()
{
_playerState.SendEvent("slam");
}
public void SlamStarted()
{
SetHorizontalVelocity(Vector2.Zero);
SetVerticalVelocity(-SlamSpeed);
}
public void HandleSlam(float delta)
{
if (isOnFloorCustom()) _playerState.SendEvent("grounded");
}
public void SlamEnded()
{
}
///////////////////////////
// Empowerement management //
///////////////////////////
public void PowerRecharging(float delta)
{
var progress = (float) (_powerCooldownTimer.TimeLeft / _powerCooldownTimer.WaitTime);
PowerCooldownIndicator.SetCustomMinimumSize(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 == MaxNumberOfEmpoweredActions ? "fully_charged" : "recharge";
_playerState.SendEvent(eventToSend);
}
public bool CanPerformEmpoweredAction()
{
return EmpoweredActionsLeft > 0 && TutorialDone;
}
public void PerformEmpoweredAction()
{
_isWallJumpAvailable = true;
EmpoweredActionsLeft--;
_playerState.SendEvent(EmpoweredActionsLeft <= 0 ? "expired" : "power_used");
}
///////////////////////////
// Aim Management ///////
///////////////////////////
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("cancel");
DashSystem.StopPreparingDash();
}
public void ReduceTimeScaleWhileAiming()
{
Engine.SetTimeScale(TimeScaleAimInAir);
_timeScaleAimInAirTimer.Start();
}
public void ResetTimeScale()
{
Engine.SetTimeScale(1);
}
public void OnAimingEntered()
{
if (!CanPerformEmpoweredAction())
return;
DashSystem.StartPreparingDash();
DashIndicatorMesh.Visible = true;
if (!isOnFloorCustom())
ReduceTimeScaleWhileAiming();
}
public void HandleAiming(float delta)
{
if (WeaponSystem.InHandState.Active)
RotateWeaponWithPlayer();
DashIndicatorMeshCylinder.Height = DashSystem.PlannedLocation.DistanceTo(GlobalPosition);
DashIndicatorNode.LookAt(DashSystem.PlannedLocation);
if (CanPerformEmpoweredAction())
DashSystem.PrepareDash();
}
public void OnAimingExited()
{
DashSystem.StopPreparingDash();
DashIndicatorMesh.Visible = false;
}
///////////////////////////
// Parry Management ///////
///////////////////////////
public void OnInputParryPressed()
{
if (WeaponSystem.FlyingState.Active)
{
DashToFlyingWeapon();
return;
}
if (WeaponSystem.PlantedState.Active)
{
DashToPlantedWeapon();
}
}
///////////////////////////
// Powered dash ///////
///////////////////////////
public void OnAimedDashStarted()
{
// Adjusting for player height, where the middle of the capsule should get to the dash location instead of the
// feet of the capsule
var correction = DashSystem.CollisionNormal == Vector3.Down ? _playerHeight : DashSystem.DashCastRadius;
var correctedLocation = DashSystem.PlannedLocation + Vector3.Down * correction;
_preDashVelocity = Velocity;
_dashDirection = (correctedLocation - GlobalPosition).Normalized();
var dashTween = CreatePositionTween(correctedLocation, AimedDashTime);
// dashTween.TweenMethod(Callable.From<float>(AimedDashTweenOngoing), 0.0f, 1.0f, AimedDashTime);
dashTween.Finished += AimedDashTweenEnded;
_customMantle = DashSystem.ShouldMantle;
_customMantleCurve = DashSystem.MantleSystem.MantleCurve;
_customMantleStartTransform = DashSystem.MantleSystem.GlobalTransform;
}
public void AimedDashTweenEnded()
{
var dashEvent = isOnFloorCustom() ? "grounded" : "dash_finished";
_playerState.SendEvent(dashEvent);
}
public void OnAimedDashFinished()
{
var postDashVelocity = _preDashVelocity.Length() > PostDashSpeed ? PostDashSpeed : _preDashVelocity.Length();
Velocity = _dashDirection * postDashVelocity;
if (_customMantle) _playerState.SendEvent("mantle");
}
// Weapon dashing
public void ThrowWeapon()
{
_playerState.SendEvent("cancel_aim");
RemoveChildNode(WeaponRoot);
var weaponTargetLocation = DashSystem.HasHit ? DashSystem.CollisionPoint : DashSystem.PlannedLocation;
WeaponSystem.ThrowWeapon(
weaponTargetLocation,
DashSystem.HasHit,
DashSystem.CollisionPoint,
DashSystem.CollisionNormal);
}
public void RecoverWeapon()
{
RecoverChildNode(WeaponRoot);
WeaponSystem.ResetWeapon();
}
public void DashToFlyingWeapon()
{
_playerState.SendEvent("cancel_aim");
_playerState.SendEvent("weapon_dash");
PerformEmpoweredAction();
DashSystem.ShouldMantle = false;
_dashDirection = (WeaponSystem.GlobalPosition - GlobalPosition).Normalized();
var dashTween = CreatePositionTween(WeaponSystem.GlobalPosition, AimedDashTime);
dashTween.Finished += DashToFlyingWeaponTweenEnded;
}
public void DashToFlyingWeaponTweenEnded()
{
RecoverWeapon();
var vel = _dashDirection * PostDashSpeed;
SetVelocity(vel);
_playerState.SendEvent("dash_finished");
}
public void DashToPlantedWeapon()
{
_playerState.SendEvent("cancel_aim");
_playerState.SendEvent("weapon_dash");
PerformEmpoweredAction();
DashSystem.ShouldMantle = false;
var dashLocation = WeaponSystem.PlantLocation;
if (WeaponSystem.IsPlantedInWall())
dashLocation += WeaponSystem.PlantNormal * _playerRadius;
if (WeaponSystem.IsPlantedUnderPlatform())
dashLocation += Vector3.Down * _playerHeight;
_wallHugStartNormal = WeaponSystem.PlantNormal;
_currentWallContactPoint = WeaponSystem.PlantLocation;
_wallHugStartLocation = dashLocation;
_wallHugStartProjectedVelocity = Velocity.Slide(_wallHugStartNormal);
var dashTween = CreatePositionTween(dashLocation, AimedDashTime);
dashTween.Finished += DashToPlantedWeaponTweenEnded;
}
public void DashToPlantedWeaponTweenEnded()
{
// Store the weapon state before resetting it
var isPlantedOnWall = WeaponSystem.IsPlantedInWall();
var isPlantedUnderPlatform = WeaponSystem.IsPlantedUnderPlatform();
var shouldDashToHanging = isPlantedOnWall || isPlantedUnderPlatform;
RecoverWeapon();
var resultingEvent = shouldDashToHanging ? "dash_to_planted" : "dash_finished";
_playerState.SendEvent(resultingEvent);
}
///////////////////////////
// Processes //////////////
///////////////////////////
public override void _PhysicsProcess(double delta)
{
if (_currentInputBufferFrames > 0) _currentInputBufferFrames -= 1;
LookAround(delta);
CameraModifications((float) delta);
MoveSlideAndHandleStairs((float) delta);
MantleSystem.ProcessMantle(_grounded.Active);
if (WeaponSystem.InHandState.Active)
RotateWeaponWithPlayer();
if (WeaponSystem.InHandState.Active && !_aiming.Active && TutorialDone)
{
DashIndicatorMesh.Visible = false;
}
if (!WeaponSystem.InHandState.Active && TutorialDone)
{
DashIndicatorMesh.Visible = true;
DashIndicatorMeshCylinder.Height = WeaponSystem.GlobalPosition.DistanceTo(GlobalPosition) * 2;
DashIndicatorNode.LookAt(WeaponSystem.GlobalPosition);
}
}
public float TakeDamage(RDamage damage)
{
return CDamage.TakeDamage(damage);
}
///////////////////////////
// Hit Management ///////
///////////////////////////
public void OnInputHitPressed()
{
if (_aiming.Active && WeaponSystem.InHandState.Active)
{
ThrowWeapon();
}
if (!WeaponSystem.InHandState.Active) return;
var bodies = WeaponHitbox.GetOverlappingBodies();
foreach (var body in bodies)
{
if(body is IDamageable spawnable)
spawnable.TakeDamage(GetDamageDealt);
}
}
}