using System; using System.Collections.Generic; using Godot; using GodotStateCharts; using Movementtests.addons.godot_state_charts.csharp; using Movementtests.interfaces; using Movementtests.systems; using Movementtests.player_controller.Scripts; using RustyOptions; [GlobalClass, Icon("res://assets/ui/IconGodotNode/node_3D/icon_character.png")] 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 // /////////////////////////// [Signal] public delegate void PlayerDiedEventHandler(); public event Action DamageTaken; public event Action HealthChanged; public event Action 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; } [ExportGroup("Targeting")] [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; [ExportGroup("Instantiation")] [Export] public PackedScene Explosion { get; set; } // 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 NumOfHeadCollisionDetectors = 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 StateChartState _parryStandard; private StateChartState _parryDash; 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 _hitEnemies = new List(); private ShapeCast3D _closeEnemyDetector; private Camera3D _camera; public override void _Ready() { LoadSettings(); /////////////////////////// // Getting components ///// /////////////////////////// // General use stuff PlayerUi = GetNode("UI"); _closeEnemyDetector = GetNode("%CloseEnemyDetector"); _closeEnemyDetector.TargetPosition = _closeEnemyDetector.TargetPosition.Normalized() * TargetingDistance; // DashIndicator = GetNode("%DashIndicator"); PowerCooldownIndicator = GetNode("%DashCooldownIndicator"); PowerCooldownIndicator.Visible = false; EmpoweredActionsLeft = MaxNumberOfEmpoweredActions; _targetSpeed = WalkSpeed; DashIndicatorNode = GetNode("DashIndicator"); DashIndicatorMesh = GetNode("DashIndicator/DashIndicatorMesh"); DashIndicatorMeshCylinder = DashIndicatorMesh.Mesh as CylinderMesh; DashIndicatorMesh.Visible = false; SfxPlayer = GetNode("SFXPlayer"); _audioStream = SfxPlayer.GetStreamPlayback() as AudioStreamPlaybackInteractive; // Camera stuff HeadSystem = GetNode("HeadSystem"); _camera = GetNode("HeadSystem/CameraSmooth/Camera3D"); Node3D cameraSmooth = GetNode("HeadSystem/CameraSmooth"); // Movement stuff WeaponSystem = GetNode("WeaponSystem"); MantleSystem = GetNode("HeadSystem/MantleSystem"); StandingCollider = GetNode("StandingCollider"); SlideCollider = GetNode("SlideCollider"); DashSystem = GetNode("DashSystem"); StairsSystem = GetNode("StairsSystem"); WallHugSystem = GetNode("WallHugSystem"); WallRunSnapper = GetNode("%WallRunSnapper"); GroundDetector = GetNode("GroundDetector"); CeilingDetector = GetNode("CeilingDetector"); DirectGroundDetector = GetNode("DirectGroundDetector"); DashDamageDetector = GetNode("DashDamage"); SlidingEnemyDetector = GetNode("SlidingEnemyDetector"); RayCast3D stairsBelowRayCast3D = GetNode("StairsBelowRayCast3D"); RayCast3D stairsAheadRayCast3D = GetNode("StairsAheadRayCast3D"); _headCollisionDetectors = new RayCast3D[NumOfHeadCollisionDetectors]; for (int i = 0; i < NumOfHeadCollisionDetectors; i++) { _headCollisionDetectors[i] = GetNode( "HeadCollisionDetectors/HeadCollisionDetector" + i); } var playerShape = StandingCollider.GetShape() as CapsuleShape3D; _playerHeight = playerShape!.Height; _playerRadius = playerShape.Radius; // Combat stuff WeaponHitbox = GetNode("%WeaponHitbox"); WeaponHitbox.Monitoring = false; WeaponHitbox.BodyEntered += RegisterHitEnnemy; CHealth = GetNode("CHealth") as CHealth; CKnockback = GetNode("CKnockback") as CKnockback; CDamageable = GetNode("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; PlayerUi.Initialize(CHealth.CurrentHealth); CDamageable.DamageTaken += (damageable, record) => ReduceHealth(damageable, record); CDamageable.DamageTaken += (_, record) => RegisterKnockback(new KnockbackRecord(record)); CHealth.HealthChanged += PlayerUi.OnHealthChanged; 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")); _parryStandard = StateChartState.Of(GetNode("StateChart/Root/Attack/StandardParry")); _parryDash = StateChartState.Of(GetNode("StateChart/Root/Attack/DashParry")); // State timers _powerCooldownTimer = GetNode("PowerCooldown"); _timeScaleAimInAirTimer = GetNode("TimeScaleAimInAir"); _simpleDashCooldownTimer = GetNode("DashCooldown"); _airborneDashCooldownTimer = GetNode("AirborneDashCooldown"); _invincibilityTimer = GetNode("InvincibilityTime"); _attackCooldown = GetNode("AttackCooldown"); /////////////////////////// // Initialize components // /////////////////////////// // Camera stuff HeadSystem.Init(); HeadSystem.HitboxActivated += OnHitboxActivated; HeadSystem.HitboxDeactivated += OnHitboxDeactivated; HeadSystem.StepFoot += OnFootStepped; HeadSystem.DeathAnimationFinished += OnDeathAnimationFinished; // 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; _parryStandard.StateEntered += OnStandardParryStarted; _parryDash.StateEntered += OnDashParryStarted; // Testing out kill // GetTree().CreateTimer(2).Timeout += () => Kill(this); } /////////////////////////// // 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 < NumOfHeadCollisionDetectors; 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"); if (Explosion.Instantiate() is not Explosion explosion) return; explosion.Radius = 10f; GetTree().GetRoot().AddChild(explosion); explosion.GlobalPosition = GlobalPosition; } /////////////////////////// // 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() { var attackToDo = _isEnemyInDashAttackRange ? "dash_parry" : "standard_parry"; _playerState.SendEvent(attackToDo); } /////////////////////////// // 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(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.TargetWouldKill; _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); var wouldKill = false; if (_targetObject is IHealthable h and IDamageable d) { var wouldBeDamage = d.ComputeDamage(new DamageRecord(GlobalPosition, RDamage)); if (h.CurrentHealth < wouldBeDamage.Damage.DamageDealt) wouldKill = true; } _isEnemyInDashAttackRange = true; enemyTargetState = wouldKill ? PlayerUi.TargetState.TargetWouldKill : PlayerUi.TargetState.TargetWouldNotKill; 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"); } public void OnStandardParryStarted() { _attackCooldown.Start(); HeadSystem.OnParry(); _audioStream!.SwitchToClipByName("parry"); } // TODO: fix repeated code and improve parry knockback 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 OnDashParryStarted() { _audioStream!.SwitchToClipByName("parry"); _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 += OnDashParryEnded; } public void OnDashAttackEnded() { if (_targetObject is IDamageable damageable) { _hitEnemies.Add(damageable); TriggerDamage(); } if (_targetObject is IStunnable stunnable) { stunnable.Stun(); } GlobalPosition = ComputePositionAfterTargetedDash(_targetLocation, _targetHitLocation); var postDashVelocity = _preDashVelocity.Length() > PostDashSpeed ? _preDashVelocity.Length() : PostDashSpeed; Velocity = _dashDirection * postDashVelocity; _isInvincible = false; _playerState.SendEvent("attack_finished"); } public void OnDashParryEnded() { if (_targetObject is IDamageable damageable) { _hitEnemies.Add(damageable); TriggerDamage(); } if (_targetObject is IStunnable stunnable) { stunnable.Stun(); } Velocity = -_dashDirection*RKnockback.Modifier; _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; } if (WeaponSystem.FlyingState.Active) { DashToFlyingWeapon(); return; } if (WeaponSystem.PlantedState.Active) { DashToPlantedWeapon(); 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(GlobalPosition, 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(KnockbackRecord knockbackRecord) { CKnockback.RegisterKnockback(knockbackRecord); } public Vector3 ComputeKnockback() { var kb = CKnockback.ComputeKnockback(); return kb; } public void Kill() { HeadSystem.OnStartDeathAnimation(); } public void OnDeathAnimationFinished() { EmitSignalPlayerDied(); } public void ResetInvincibility() { _isInvincible = false; } // Sound public void OnFootStepped() { _audioStream!.SwitchToClipByName("footsteps"); } }