2176 lines
68 KiB
C#
2176 lines
68 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Godot;
|
|
using GodotStateCharts;
|
|
using Movementtests.addons.godot_state_charts.csharp;
|
|
using Movementtests.interfaces;
|
|
using Movementtests.systems;
|
|
using Movementtests.player_controller.Scripts;
|
|
using Movementtests.systems.damage;
|
|
using RustyOptions;
|
|
|
|
public partial class PlayerController : CharacterBody3D,
|
|
IDamageable,
|
|
IDamageDealer,
|
|
IHealthable,
|
|
IKnockbackable
|
|
{
|
|
// 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 event Action<IDamageable, DamageRecord> DamageTaken;
|
|
public event Action<IHealthable, HealthChangedRecord> HealthChanged;
|
|
public event Action<IHealthable> HealthDepleted;
|
|
|
|
///////////////////////////
|
|
// Public stuff //
|
|
///////////////////////////
|
|
public HeadSystem HeadSystem;
|
|
public StairsSystem StairsSystem;
|
|
public MantleSystem MantleSystem;
|
|
public DashSystem DashSystem;
|
|
public CollisionShape3D StandingCollider;
|
|
public CollisionShape3D SlideCollider;
|
|
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;
|
|
public AudioStreamPlayer3D SfxPlayer;
|
|
|
|
public ShapeCast3D DashDamageDetector;
|
|
public Area3D SlidingEnemyDetector;
|
|
|
|
// Inspector stuff
|
|
[Export] public Marker3D TutorialWeaponTarget;
|
|
[Export] public bool TutorialDone { get; set; }
|
|
|
|
// Combat stuff
|
|
[ExportCategory("Combat")]
|
|
[ExportGroup("Damage")]
|
|
[Export] public RDamage RDamage { get; set; }
|
|
[Export] public RKnockback RKnockback { get; set; }
|
|
[Export] public RHealth RHealth { get; set; }
|
|
|
|
[Export(PropertyHint.Range, "0,20,0.1,or_greater")]
|
|
public float TargetingDistance { get; set; } = 10.0f;
|
|
|
|
[Export(PropertyHint.Range, "0,20,0.1,or_greater")]
|
|
public float TargetInRangeDistance { get; set; } = 5.0f;
|
|
|
|
// Movement stuff
|
|
[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;
|
|
private AudioStreamPlaybackInteractive _audioStream;
|
|
|
|
// 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;
|
|
private Timer _invincibilityTimer;
|
|
private Timer _attackCooldown;
|
|
|
|
// 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 _weaponDash;
|
|
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 StateChartState _attackStandard;
|
|
private StateChartState _attackDash;
|
|
|
|
private Transition _onJumpFromWall;
|
|
private Transition _onJumpFromWallFalling;
|
|
private Transition _onLeaveWallFromRun;
|
|
private Transition _onAirborneToGrounded;
|
|
|
|
private Transition _onGroundSlideJump;
|
|
private Transition _onAirGlideDoubleJump;
|
|
|
|
// Damage
|
|
public CDamageable CDamageable { get; set; }
|
|
public CHealth CHealth { get; set; }
|
|
public CKnockback CKnockback { get; set; }
|
|
public float CurrentHealth { get; set; }
|
|
|
|
private bool _isInvincible;
|
|
private readonly List<IDamageable> _hitEnemies = new List<IDamageable>();
|
|
|
|
private ShapeCast3D _closeEnemyDetector;
|
|
private Camera3D _camera;
|
|
|
|
public override void _Ready()
|
|
{
|
|
LoadSettings();
|
|
|
|
///////////////////////////
|
|
// Getting components /////
|
|
///////////////////////////
|
|
|
|
// General use stuff
|
|
PlayerUi = GetNode<PlayerUi>("UI");
|
|
_closeEnemyDetector = GetNode<ShapeCast3D>("%CloseEnemyDetector");
|
|
_closeEnemyDetector.TargetPosition = _closeEnemyDetector.TargetPosition.Normalized() * TargetingDistance;
|
|
|
|
// 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;
|
|
|
|
SfxPlayer = GetNode<AudioStreamPlayer3D>("SFXPlayer");
|
|
_audioStream = SfxPlayer.GetStreamPlayback() as AudioStreamPlaybackInteractive;
|
|
|
|
// Camera stuff
|
|
HeadSystem = GetNode<HeadSystem>("HeadSystem");
|
|
_camera = GetNode<Camera3D>("HeadSystem/CameraSmooth/Camera3D");
|
|
Node3D cameraSmooth = GetNode<Node3D>("HeadSystem/CameraSmooth");
|
|
|
|
// Movement stuff
|
|
WeaponSystem = GetNode<WeaponSystem>("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");
|
|
DashDamageDetector = GetNode<ShapeCast3D>("DashDamage");
|
|
SlidingEnemyDetector = GetNode<Area3D>("SlidingEnemyDetector");
|
|
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;
|
|
|
|
// Combat stuff
|
|
WeaponHitbox = GetNode<Area3D>("%WeaponHitbox");
|
|
WeaponHitbox.Monitoring = false;
|
|
WeaponHitbox.BodyEntered += RegisterHitEnnemy;
|
|
|
|
CHealth = GetNode<Node>("CHealth") as CHealth;
|
|
CKnockback = GetNode<Node>("CKnockback") as CKnockback;
|
|
CDamageable = GetNode<Node>("CDamageable") as CDamageable;
|
|
if (CHealth == null) throw new Exception("CHealth not found!");
|
|
if (CKnockback == null) throw new Exception("CKnockback not found!");
|
|
if (CDamageable == null) throw new Exception("CDamageable not found!");
|
|
|
|
if (RHealth != null)
|
|
{
|
|
CHealth.RHealth = RHealth;
|
|
CHealth.CurrentHealth = RHealth.StartingHealth;
|
|
}
|
|
if (RKnockback != null) CKnockback!.RKnockback = RKnockback;
|
|
|
|
CDamageable.DamageTaken += (source, record) => ReduceHealth(source, record);
|
|
CDamageable.DamageTaken += RegisterKnockback;
|
|
CHealth.HealthDepleted += Kill;
|
|
|
|
// 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"));
|
|
_weaponDash = StateChartState.Of(GetNode("StateChart/Root/Movement/Dashing/ToWeaponDash"));
|
|
_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"));
|
|
_onAirborneToGrounded = Transition.Of(GetNode("StateChart/Root/Movement/Airborne/OnGrounded"));
|
|
|
|
// Attack states
|
|
_attackStandard = StateChartState.Of(GetNode("StateChart/Root/Attack/StandardAttack"));
|
|
_attackDash = StateChartState.Of(GetNode("StateChart/Root/Attack/DashAttack"));
|
|
|
|
// State timers
|
|
_powerCooldownTimer = GetNode<Timer>("PowerCooldown");
|
|
_timeScaleAimInAirTimer = GetNode<Timer>("TimeScaleAimInAir");
|
|
_simpleDashCooldownTimer = GetNode<Timer>("DashCooldown");
|
|
_airborneDashCooldownTimer = GetNode<Timer>("AirborneDashCooldown");
|
|
_invincibilityTimer = GetNode<Timer>("InvincibilityTime");
|
|
_attackCooldown = GetNode<Timer>("AttackCooldown");
|
|
|
|
///////////////////////////
|
|
// Initialize components //
|
|
///////////////////////////
|
|
|
|
// Camera stuff
|
|
HeadSystem.Init();
|
|
HeadSystem.HitboxActivated += OnHitboxActivated;
|
|
HeadSystem.HitboxDeactivated += OnHitboxDeactivated;
|
|
HeadSystem.StepFoot += OnFootStepped;
|
|
|
|
// 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();
|
|
WallHugSystem.Init();
|
|
|
|
EmpoweredActionsLeft = MaxNumberOfEmpoweredActions;
|
|
|
|
if (!TutorialDone)
|
|
PlaceWeaponForTutorial();
|
|
|
|
///////////////////////////
|
|
// Signal setup ///////////
|
|
///////////////////////////
|
|
_invincibilityTimer.Timeout += ResetInvincibility;
|
|
_attackCooldown.Timeout += ResetAttackCooldown;
|
|
|
|
_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;
|
|
|
|
_weaponDash.StateExited += OnWeaponDashFinished;
|
|
|
|
SlidingEnemyDetector.BodyEntered += EnemyHitWhileSliding;
|
|
_sliding.StateEntered += SlideStarted;
|
|
_sliding.StateExited += SlideEnded;
|
|
_slideCanceled.StateEntered += OnSlideCanceled;
|
|
_slideCanceled.StatePhysicsProcessing += HandleSlideCanceled;
|
|
_groundSliding.StatePhysicsProcessing += HandleGroundSlide;
|
|
_groundSliding.StateEntered += OnGroundSlideStarted;
|
|
_airGliding.StatePhysicsProcessing += HandleAirGlide;
|
|
_airGlidingDoubleJump.StatePhysicsProcessing += HandleAirGlide;
|
|
_airGliding.StateEntered += OnAirGlideStarted;
|
|
_airGlidingDoubleJump.StateEntered += OnAirGlideStarted;
|
|
|
|
_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;
|
|
|
|
_onWallHanging.StateExited += RecoverWeapon;
|
|
// _onDashEnded.Taken += RecoverWeapon;
|
|
|
|
_onJumpFromWall.Taken += OnJumpFromWall;
|
|
_onJumpFromWallFalling.Taken += OnJumpFromWall;
|
|
_onLeaveWallFromRun.Taken += OnLeaveWallFromRun;
|
|
_onAirborneToGrounded.Taken += OnAirborneToGrounded;
|
|
|
|
// Attack states
|
|
_attackStandard.StateEntered += OnStandardAttackStarted;
|
|
_attackDash.StateEntered += OnDashAttackStarted;
|
|
}
|
|
|
|
///////////////////////////
|
|
// 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(WeaponSystem);
|
|
GetTree().GetRoot().CallDeferred(Node.MethodName.AddChild, WeaponSystem);
|
|
WeaponSystem.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 void OnAirborneToGrounded()
|
|
{
|
|
HeadSystem.OnJumpEnded();
|
|
_audioStream!.SwitchToClipByName("land");
|
|
}
|
|
|
|
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 moveInput = GetGlobalMoveInput();
|
|
|
|
var lookAroundInputs = new HeadSystem.CameraParameters(
|
|
Delta: delta,
|
|
LookDir: inputLookDir,
|
|
PlayerInput: moveInput,
|
|
PlayerVelocity:Velocity,
|
|
WallContactPoint: wallHugContactPoint,
|
|
SensitivitMultiplier: lookSensitivity,
|
|
WithCameraJitter: _groundSliding.Active,
|
|
WithCameraBobbing: _grounded.Active || _onWallRunning.Active,
|
|
BobbingMultiplier: _headBobbingMultiplier,
|
|
FovMultiplier: _fovChangeMultiplier);
|
|
HeadSystem.LookAround(lookAroundInputs);
|
|
}
|
|
public Vector3 GetGlobalForwardFacingVector()
|
|
{
|
|
return Transform.Basis * HeadSystem.Transform.Basis * Vector3.Forward;
|
|
}
|
|
// 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)
|
|
{
|
|
node.GetParent().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)
|
|
{
|
|
_audioStream.SwitchToClipByName("dash");
|
|
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)
|
|
{
|
|
HeadSystem.OnJumpStarted();
|
|
_audioStream!.SwitchToClipByName("jump");
|
|
|
|
_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();
|
|
_audioStream!.SwitchToClipByName("mantle");
|
|
|
|
_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 tween = GetTree().CreateTween();
|
|
tween.SetTrans(Tween.TransitionType.Linear);
|
|
tween.SetEase(Tween.EaseType.In);
|
|
tween.TweenProperty(_mantlePath.PathFollow, "progress_ratio", 1, MantleTime);
|
|
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;
|
|
|
|
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 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, 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();
|
|
_audioStream!.SwitchToClipByName("glide");
|
|
SetupSlideCollision();
|
|
|
|
SlidingEnemyDetector.Monitoring = true;
|
|
_isInvincible = true;
|
|
}
|
|
|
|
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 OnGroundSlideStarted()
|
|
{
|
|
}
|
|
public void OnAirGlideStarted()
|
|
{
|
|
}
|
|
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()
|
|
{
|
|
SlidingEnemyDetector.Monitoring = false;
|
|
_isInvincible = false;
|
|
|
|
SetupStandingCollision();
|
|
_audioStream!.SwitchToClipByName("footsteps");
|
|
_targetSpeed = WalkSpeed;
|
|
}
|
|
|
|
public void EnemyHitWhileSliding(Node enemy)
|
|
{
|
|
if(enemy is not IDamageable damageable)
|
|
return;
|
|
_hitEnemies.Add(damageable);
|
|
TriggerDamage();
|
|
}
|
|
|
|
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);
|
|
_audioStream!.SwitchToClipByName("dash");
|
|
}
|
|
public void HandleSlam(float delta)
|
|
{
|
|
if (isOnFloorCustom()) _playerState.SendEvent("grounded");
|
|
}
|
|
public void SlamEnded()
|
|
{
|
|
HeadSystem.OnGetHit();
|
|
_audioStream!.SwitchToClipByName("slam");
|
|
}
|
|
|
|
///////////////////////////
|
|
// 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;
|
|
|
|
// DashIndicatorMesh.Visible = true;
|
|
if (!isOnFloorCustom())
|
|
ReduceTimeScaleWhileAiming();
|
|
}
|
|
public void HandleAiming(float delta)
|
|
{
|
|
// 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()
|
|
{
|
|
_audioStream.SwitchToClipByName("dash");
|
|
|
|
// 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;
|
|
|
|
if (DashSystem.CanDashThroughTarget && DashSystem.CollidedObject is ITargetable targetable)
|
|
correctedLocation = ComputePositionAfterTargetedDash(targetable.GetTargetGlobalPosition(), DashSystem.CollisionPoint);
|
|
|
|
// Start invincibility timer for the duration of the dash and a bit more afterwards
|
|
OnHitInvincibility();
|
|
|
|
_preDashVelocity = Velocity;
|
|
_dashDirection = (correctedLocation - GlobalPosition).Normalized();
|
|
SetupDashDamageDetector(correctedLocation);
|
|
|
|
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 SetupDashDamageDetector(Vector3 endDashLocation)
|
|
{
|
|
RemoveChildNode(DashDamageDetector);
|
|
DashDamageDetector.SetTargetPosition(DashDamageDetector.ToLocal(endDashLocation));
|
|
DashDamageDetector.Enabled = true;
|
|
}
|
|
|
|
public void ComputeDashDamage()
|
|
{
|
|
for (var i = 0; i < DashDamageDetector.GetCollisionCount(); i++)
|
|
{
|
|
var collidedObject = DashDamageDetector.GetCollider(i);
|
|
if (collidedObject is not IDamageable damageable) continue;
|
|
_hitEnemies.Add(damageable);
|
|
}
|
|
TriggerDamage();
|
|
|
|
RecoverChildNode(DashDamageDetector);
|
|
DashDamageDetector.Enabled = false;
|
|
}
|
|
|
|
public void AimedDashTweenEnded()
|
|
{
|
|
var dashEvent = isOnFloorCustom() ? "grounded" : "dash_finished";
|
|
_playerState.SendEvent(dashEvent);
|
|
}
|
|
public void OnAimedDashFinished()
|
|
{
|
|
ManageAttackedEnemyPostDash(DashSystem.CollidedObject as Node);
|
|
DashSystem.CollidedObject = null;
|
|
ComputeDashDamage();
|
|
|
|
if (_customMantle)
|
|
{
|
|
_playerState.SendEvent("mantle");
|
|
return;
|
|
}
|
|
var postDashVelocity = _preDashVelocity.Length() > PostDashSpeed ? _preDashVelocity.Length() : PostDashSpeed;
|
|
Velocity = _dashDirection * postDashVelocity;
|
|
}
|
|
|
|
// Weapon dashing
|
|
public void ThrowWeapon()
|
|
{
|
|
_audioStream.SwitchToClipByName("attacks");
|
|
|
|
_playerState.SendEvent("cancel_aim");
|
|
RemoveChildNode(WeaponSystem);
|
|
HeadSystem.HideWeapon();
|
|
|
|
var weaponTargetLocation = DashSystem.HasHit ? DashSystem.CollisionPoint : DashSystem.PlannedLocation;
|
|
WeaponSystem.ThrowWeapon(
|
|
weaponTargetLocation,
|
|
DashSystem.HasHit,
|
|
DashSystem.CollisionPoint,
|
|
DashSystem.CollisionNormal,
|
|
DashSystem.CollidedObject as Node);
|
|
}
|
|
public void RecoverWeapon()
|
|
{
|
|
if (WeaponSystem.GetParent() == this) return;
|
|
|
|
HeadSystem.ShowWeapon();
|
|
WeaponSystem.ResetWeapon();
|
|
RecoverChildNode(WeaponSystem);
|
|
}
|
|
|
|
public void DashToFlyingWeapon()
|
|
{
|
|
_playerState.SendEvent("cancel_aim");
|
|
_playerState.SendEvent("weapon_dash");
|
|
|
|
PerformEmpoweredAction();
|
|
_audioStream.SwitchToClipByName("dash");
|
|
// Start invincibility timer for the duration of the dash and a bit more afterwards
|
|
OnHitInvincibility();
|
|
|
|
SetupDashDamageDetector(WeaponSystem.GlobalPosition);
|
|
|
|
DashSystem.ShouldMantle = false;
|
|
_dashDirection = (WeaponSystem.GlobalPosition - GlobalPosition).Normalized();
|
|
|
|
var dashTween = CreatePositionTween(WeaponSystem.GlobalPosition, AimedDashTime);
|
|
dashTween.Finished += DashToFlyingWeaponTweenEnded;
|
|
}
|
|
public void DashToFlyingWeaponTweenEnded()
|
|
{
|
|
RecoverWeapon();
|
|
ComputeDashDamage();
|
|
|
|
var vel = _dashDirection * PostDashSpeed;
|
|
SetVelocity(vel);
|
|
_playerState.SendEvent("dash_finished");
|
|
}
|
|
|
|
public void DashToPlantedWeapon()
|
|
{
|
|
_playerState.SendEvent("cancel_aim");
|
|
_playerState.SendEvent("weapon_dash");
|
|
|
|
PerformEmpoweredAction();
|
|
_audioStream.SwitchToClipByName("dash");
|
|
// Start invincibility timer for the duration of the dash and a bit more afterwards
|
|
OnHitInvincibility();
|
|
|
|
DashSystem.ShouldMantle = false;
|
|
var dashLocation = WeaponSystem.PlantLocation;
|
|
if (WeaponSystem.IsPlantedInWall())
|
|
dashLocation += WeaponSystem.PlantNormal * _playerRadius;
|
|
if (WeaponSystem.IsPlantedUnderPlatform())
|
|
dashLocation += Vector3.Down * _playerHeight;
|
|
if (WeaponSystem.PlantObject is ITargetable targetable)
|
|
dashLocation = targetable.GetTargetGlobalPosition();
|
|
|
|
_wallHugStartNormal = WeaponSystem.PlantNormal;
|
|
_currentWallContactPoint = WeaponSystem.PlantLocation;
|
|
_wallHugStartLocation = dashLocation;
|
|
_wallHugStartProjectedVelocity = Velocity.Slide(_wallHugStartNormal);
|
|
|
|
SetupDashDamageDetector(dashLocation);
|
|
|
|
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 isPlantedInTarget = WeaponSystem.PlantObject is ITargetable;
|
|
var shouldDashToHanging = (isPlantedOnWall || isPlantedUnderPlatform) && !isPlantedInTarget;
|
|
var resultingEvent = shouldDashToHanging ? "dash_to_planted" : "dash_finished";
|
|
|
|
if (!shouldDashToHanging) RecoverWeapon(); // Manually recover weapon before enemy is freed in case it owns the weapon object
|
|
if (WeaponSystem.PlantObject is ITargetable)
|
|
{
|
|
HeadSystem.OnMantle(); // Recycle mantle animation
|
|
SetVerticalVelocity(PostDashSpeed);
|
|
}
|
|
|
|
ManageAttackedEnemyPostDash(WeaponSystem.PlantObject);
|
|
WeaponSystem.PlantObject = null;
|
|
ComputeDashDamage();
|
|
|
|
_playerState.SendEvent(resultingEvent);
|
|
}
|
|
|
|
public void ManageAttackedEnemyPostDash(Node enemy)
|
|
{
|
|
if (enemy is IDamageable damageable)
|
|
{
|
|
_hitEnemies.Add(damageable);
|
|
TriggerDamage();
|
|
}
|
|
if (enemy is IStunnable stunnable)
|
|
stunnable.Stun();
|
|
}
|
|
|
|
public void OnWeaponDashFinished()
|
|
{
|
|
}
|
|
|
|
///////////////////////////
|
|
// Processes //////////////
|
|
///////////////////////////
|
|
public override void _PhysicsProcess(double delta)
|
|
{
|
|
_spaceState = GetWorld3D().DirectSpaceState;
|
|
|
|
if (_currentInputBufferFrames > 0) _currentInputBufferFrames -= 1;
|
|
|
|
// Manage head and camera movement
|
|
LookAround(delta);
|
|
|
|
// Manage general movement
|
|
Velocity += ComputeKnockback();
|
|
MoveSlideAndHandleStairs((float) delta);
|
|
|
|
// Manage gameplay systems
|
|
MantleSystem.ProcessMantle(_grounded.Active);
|
|
HandleEnemyTargeting();
|
|
|
|
// Manage dash target and tutorial specific stuff
|
|
// 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);
|
|
// }
|
|
}
|
|
|
|
///////////////////////////
|
|
// Hit Management ///////
|
|
///////////////////////////
|
|
|
|
private bool _isEnemyInDashAttackRange;
|
|
private Vector3 _targetHitLocation;
|
|
private Vector3 _targetLocation;
|
|
private Object _targetObject;
|
|
public void HandleEnemyTargeting()
|
|
{
|
|
_isEnemyInDashAttackRange = false;
|
|
_closeEnemyDetector.SetRotation(HeadSystem.GetGlobalLookRotation());
|
|
var enemyTargetState = PlayerUi.TargetState.NoTarget;
|
|
var positionOnScreen = Vector2.Zero;
|
|
|
|
if (DashSystem.CanDashThroughTarget && DashSystem.CollidedObject is ITargetable dashTarget)
|
|
{
|
|
enemyTargetState = PlayerUi.TargetState.TargetDashThrough;
|
|
_targetLocation = dashTarget.GetTargetGlobalPosition();
|
|
positionOnScreen = _camera.UnprojectPosition(_targetLocation);
|
|
PlayerUi.SetEnemyTargetProperties(new PlayerUi.TargetProperties(enemyTargetState, positionOnScreen));
|
|
return;
|
|
}
|
|
|
|
|
|
if (!_closeEnemyDetector.IsColliding())
|
|
{
|
|
PlayerUi.SetEnemyTargetProperties(new PlayerUi.TargetProperties(enemyTargetState, positionOnScreen));
|
|
return;
|
|
}
|
|
|
|
_targetHitLocation = _closeEnemyDetector.GetCollisionPoint(0);
|
|
_targetObject = _closeEnemyDetector.GetCollider(0);
|
|
if (_targetObject is not ITargetable target)
|
|
{
|
|
PlayerUi.SetEnemyTargetProperties(new PlayerUi.TargetProperties(enemyTargetState, positionOnScreen));
|
|
return;
|
|
}
|
|
|
|
_targetLocation = target.GetTargetGlobalPosition();
|
|
// var targetDistance = _targetLocation.DistanceTo(GlobalPosition);
|
|
positionOnScreen = _camera.UnprojectPosition(_targetLocation);
|
|
|
|
_isEnemyInDashAttackRange = true; //targetDistance < TargetInRangeDistance; // Removing the "almost dash" UI
|
|
if (_isEnemyInDashAttackRange)
|
|
{
|
|
enemyTargetState = PlayerUi.TargetState.TargetDashThrough;
|
|
if (_targetObject is IDamageable damageable and IHealthable healthable)
|
|
{
|
|
var wouldBeDamage = damageable.ComputeDamage(new DamageRecord(this, RDamage));
|
|
if (wouldBeDamage.Damage.DamageDealt < healthable.CurrentHealth)
|
|
enemyTargetState = PlayerUi.TargetState.TargetInRange;
|
|
}
|
|
}
|
|
else enemyTargetState = PlayerUi.TargetState.TargetTooFar;
|
|
|
|
PlayerUi.SetEnemyTargetProperties(new PlayerUi.TargetProperties(enemyTargetState, positionOnScreen));
|
|
}
|
|
|
|
public DamageRecord TakeDamage(DamageRecord damageRecord)
|
|
{
|
|
if (_isInvincible)
|
|
return damageRecord with { Damage = new RDamage(0, damageRecord.Damage.DamageType) };
|
|
|
|
var finalDamage = CDamageable.TakeDamage(damageRecord);
|
|
DamageTaken?.Invoke(this, finalDamage);
|
|
|
|
HeadSystem.OnGetHit();
|
|
_audioStream!.SwitchToClipByName("damage_taken");
|
|
TriggerHitstop();
|
|
OnHitInvincibility();
|
|
|
|
return finalDamage;
|
|
}
|
|
|
|
public DamageRecord ComputeDamage(DamageRecord damageRecord)
|
|
{
|
|
return CDamageable.ComputeDamage(damageRecord);
|
|
}
|
|
|
|
public void OnHitInvincibility()
|
|
{
|
|
_isInvincible = true;
|
|
_invincibilityTimer.Start();
|
|
}
|
|
|
|
public void OnStandardAttackStarted()
|
|
{
|
|
_attackCooldown.Start();
|
|
HeadSystem.OnHit();
|
|
_audioStream!.SwitchToClipByName("attacks");
|
|
}
|
|
|
|
private PhysicsDirectSpaceState3D _spaceState;
|
|
public void OnDashAttackStarted()
|
|
{
|
|
_audioStream!.SwitchToClipByName("attacks");
|
|
|
|
_isInvincible = true;
|
|
|
|
var plannedDashLocation = _targetLocation + Vector3.Down*HeadSystem.Position.Y;
|
|
|
|
var query = PhysicsRayQueryParameters3D.Create(HeadSystem.GlobalPosition, plannedDashLocation, DashSystem.DashCast3D.CollisionMask);
|
|
var result = _spaceState.IntersectRay(query);
|
|
if (result.Count > 0)
|
|
{
|
|
plannedDashLocation = (Vector3) result["position"];
|
|
}
|
|
|
|
var travel = plannedDashLocation - GlobalPosition;
|
|
_preDashVelocity = Velocity;
|
|
_dashDirection = travel.Normalized();
|
|
var dashTween = CreatePositionTween(plannedDashLocation, AimedDashTime);
|
|
dashTween.Finished += OnDashAttackEnded;
|
|
}
|
|
|
|
public void OnDashAttackEnded()
|
|
{
|
|
if (_targetObject is IDamageable damageable)
|
|
{
|
|
_hitEnemies.Add(damageable);
|
|
TriggerDamage();
|
|
}
|
|
|
|
if (_targetObject is IStunnable stunnable)
|
|
{
|
|
stunnable.Stun();
|
|
}
|
|
|
|
var shouldKnockback = _targetObject is IHealthable { CurrentHealth: > 0 };
|
|
if (shouldKnockback)
|
|
{
|
|
Velocity = -_dashDirection*RKnockback.Modifier;
|
|
}
|
|
else
|
|
{
|
|
GlobalPosition = ComputePositionAfterTargetedDash(_targetLocation, _targetHitLocation);
|
|
var postDashVelocity = _preDashVelocity.Length() > PostDashSpeed ? _preDashVelocity.Length() : PostDashSpeed;
|
|
Velocity = _dashDirection * postDashVelocity;
|
|
}
|
|
_isInvincible = false;
|
|
_playerState.SendEvent("attack_finished");
|
|
}
|
|
|
|
public static Vector3 ComputePositionAfterTargetedDash(Vector3 targetLocation, Vector3 targetHitLocation)
|
|
{
|
|
return targetLocation + (targetLocation - targetHitLocation);
|
|
}
|
|
|
|
public void OnInputHitPressed()
|
|
{
|
|
if (_aiming.Active && WeaponSystem.InHandState.Active)
|
|
{
|
|
ThrowWeapon();
|
|
return;
|
|
}
|
|
|
|
var attackToDo = _isEnemyInDashAttackRange ? "dash_attack" : "standard_attack";
|
|
_playerState.SendEvent(attackToDo);
|
|
}
|
|
|
|
public void ResetAttackCooldown()
|
|
{
|
|
_playerState.SendEvent("attack_finished");
|
|
}
|
|
|
|
|
|
public void OnHitboxActivated()
|
|
{
|
|
WeaponHitbox.Monitoring = true;
|
|
}
|
|
public void OnHitboxDeactivated()
|
|
{
|
|
WeaponHitbox.Monitoring = false;
|
|
TriggerDamage();
|
|
}
|
|
|
|
public void RegisterHitEnnemy(Node3D body)
|
|
{
|
|
if (body is not IDamageable damageable) return;
|
|
_hitEnemies.Add(damageable);
|
|
}
|
|
|
|
public void TriggerDamage()
|
|
{
|
|
if (_hitEnemies.Count == 0) return;
|
|
|
|
foreach (var damageable in _hitEnemies)
|
|
damageable.TakeDamage(new DamageRecord(this, RDamage));
|
|
_hitEnemies.Clear();
|
|
|
|
HeadSystem.OnHitTarget();
|
|
_audioStream!.SwitchToClipByName("hits");
|
|
TriggerHitstop();
|
|
}
|
|
|
|
public void TriggerHitstop()
|
|
{
|
|
Engine.SetTimeScale(0.01);
|
|
var timer = GetTree().CreateTimer(0.1, true, false, true);
|
|
timer.Timeout += OnHitstopEnded;
|
|
}
|
|
|
|
public void OnHitstopEnded()
|
|
{
|
|
ResetTimeScale();
|
|
}
|
|
public HealthChangedRecord ReduceHealth(IDamageable source, DamageRecord damageRecord)
|
|
{
|
|
GD.Print("That's NOT fine");
|
|
var record = CHealth.ReduceHealth(source, damageRecord);
|
|
HealthChanged?.Invoke(this, record);
|
|
return record;
|
|
}
|
|
public void RegisterKnockback(IDamageable source, DamageRecord damageRecord)
|
|
{
|
|
CKnockback.RegisterKnockback(source, damageRecord);
|
|
}
|
|
|
|
public Vector3 ComputeKnockback()
|
|
{
|
|
var kb = CKnockback.ComputeKnockback();
|
|
return kb;
|
|
}
|
|
|
|
public void Kill(IHealthable source)
|
|
{
|
|
GD.Print("Player died!");
|
|
}
|
|
|
|
public void ResetInvincibility()
|
|
{
|
|
_isInvincible = false;
|
|
}
|
|
|
|
// Sound
|
|
public void OnFootStepped()
|
|
{
|
|
_audioStream!.SwitchToClipByName("footsteps");
|
|
}
|
|
}
|