using _Game.Player.StateMachine.Necro; using DG.Tweening; using UnityEngine; using UnityEngine.Events; [AddComponentMenu("Game/Player/Necromancer"), DisallowMultipleComponent()] public class PlayerNecromancerController : MonoBehaviour { public const float StandardJumpForce = 6.5f; public const float CoyoteTime = .285f; public const float MaxJumpTime = .35f; public const float JumpRememberTime = .195f; public const float invincibleTime = 1f; //refs [SerializeField] private InputReader _inputReader = default; [SerializeField] private CollisionHelper _collisionHelper; [SerializeField] private Rigidbody2D _rb; [SerializeField] private Animator _animator; [SerializeField] private SpriteRenderer _spriteRenderer; [SerializeField] private CapsuleCollider2D _capsuleCollider; #region FSM private NecroBaseState m_currentState = null; private readonly NecroGroundState m_groundedState = new NecroGroundState(); private readonly NecroJumpingState m_jumpingState = new NecroJumpingState(); private readonly NecroFallingState m_fallingState = new NecroFallingState(); private readonly NecroDeadState m_necroDeadState = new NecroDeadState(); public NecroGroundState GroundedState => m_groundedState; public NecroJumpingState JumpingState => m_jumpingState; public NecroFallingState FallingState => m_fallingState; public NecroDeadState DeadState => m_necroDeadState; #endregion [Header("Customizables"), Space(5)] public float moveSpeed; public float jumpHeight; public float fastFallMultiplier = 2f; [SerializeField, Range(42, 120)] private float airRecoveryRate = 45f; [Space(5)] [Range(0, .3f), SerializeField] private float smoothingFactor = 0.1666f; [Range(0, .56f), SerializeField] private float airSmoothingFactor = 0.1666f; [Space(5)] private Vector2 _previousMovementInput; public bool jumpInput; private float startGravityScale; private float m_jumpForce; private readonly Vector2 capsuleColliderDeadOffset = new Vector2(0, -0.62f); private Vector2 initialVerticalCapsuleSize; private IInteractable _interactable; #region Timers private float coyoteTimer; private float jumpLimitTimer; private float rememberJumpTimer; private float invincibleFrameTimer; #endregion internal Vector2 m_velocityVar; internal float currentXVelocity; [Space(5)] public UnityEvent onWizardDead; public bool alive = true; //Accesors public Vector2 TargetMoveSpeed => (Vector2.right * moveSpeed * _previousMovementInput); public Rigidbody2D Rigidbody => _rb; public float SmoothingFactor => smoothingFactor; public CollisionHelper CollsionsH => _collisionHelper; public float CoyoteTimer { get => coyoteTimer; set => coyoteTimer = value; } public float RememberJumpTimer { get => rememberJumpTimer; set => rememberJumpTimer = value; } public float JumpTimer => jumpLimitTimer; public SpriteRenderer SpriteRenderer => _spriteRenderer; public Animator Animator => _animator; private void OnEnable() { _inputReader.jumpEvent += OnJumpInitiated; _inputReader.jumpCanceledEvent += OnJumpCanceled; _inputReader.moveEvent += OnMove; _inputReader.interactEvent += OnInteract; } private void OnDisable() { _inputReader.jumpEvent -= OnJumpInitiated; _inputReader.jumpCanceledEvent -= OnJumpCanceled; _inputReader.moveEvent -= OnMove; _inputReader.interactEvent -= OnInteract; } private void Awake() { ChangeState(m_groundedState); startGravityScale = _rb.gravityScale; initialVerticalCapsuleSize = _capsuleCollider.size; } private void Start() { GameManager.Instance.onGameOver.AddListener(() => { this.enabled = false; }); } private void OnInteract() { //ToDo: For Talking //Should work on any state but Dead if (ReferenceEquals(m_currentState, m_necroDeadState) || PauseManager.paused) { Debug.Log("Invalid State"); return; } if (!ReferenceEquals(_interactable, null)) _interactable.Interact(); } private void OnMove(Vector2 movement) { _previousMovementInput = movement; } private void OnJumpInitiated() { jumpInput = true; rememberJumpTimer = JumpRememberTime; m_currentState.OnPlayerJump(this); } private void OnJumpCanceled() { jumpInput = false; } void Update() { if (PauseManager.paused) return; //Limit Jumps if (jumpLimitTimer > 0) jumpLimitTimer -= Time.deltaTime; if (invincibleFrameTimer > 0) invincibleFrameTimer -= Time.deltaTime; m_currentState.Update(this); _rb.velocity = new Vector2(_rb.velocity.x, Mathf.Clamp(_rb.velocity.y, -10, 20f)); } private void FixedUpdate() { if (PauseManager.paused) return; m_currentState.FixedUpdate(this); } public void HandleSpriteFlip() { if (Mathf.Abs(_previousMovementInput.x) >= float.Epsilon) _spriteRenderer.flipX = _previousMovementInput.x < 0 ? true : false; } public void Walk() { var x = Mathf.SmoothDamp(_rb.velocity.x, TargetMoveSpeed.x, ref currentXVelocity, SmoothingFactor); _rb.velocity = new Vector2(x, _rb.velocity.y); } public void HandleMovementAnimation() { if (Mathf.Abs(_previousMovementInput.x) >= float.Epsilon) { Animator.Play("Walk"); Animator.SetFloat("xSpeed", Mathf.Abs(_rb.velocity.x / moveSpeed)); } else { if (Mathf.Approximately(Mathf.Floor(_rb.velocity.x), 0)) { Animator.Play("Idle"); } else { Animator.SetFloat("xSpeed", Mathf.Clamp01(Mathf.Abs(_rb.velocity.x))); } } } //Walk While in the air public void AirWalk() { var x = Mathf.SmoothDamp(_rb.velocity.x, TargetMoveSpeed.x, ref currentXVelocity, airSmoothingFactor); _rb.velocity = new Vector2(x, _rb.velocity.y); } public void AirResistMoveSimpler() { _rb.velocity = new Vector2(Approach(_rb.velocity.x, moveSpeed * _previousMovementInput.x, airRecoveryRate * .65f * Time.fixedDeltaTime), _rb.velocity.y); } public void Hop() { _rb.velocity = new Vector2(_rb.velocity.x, 0); _rb.gravityScale = startGravityScale; SetJumpForceAtDesiredHeight(); var jumpForce = m_jumpForce * 1.025f / 2; jumpLimitTimer = MaxJumpTime; float xforce = _rb.velocity.x + (Mathf.Abs(_rb.velocity.x) > 0 ? Mathf.Sign(_rb.velocity.x) * Mathf.Abs(_previousMovementInput.x) : 0) * 1.025f; _rb.velocity = new Vector2(xforce, jumpForce); } public void FrameIndependentHeldJump(float heldTime) { //ToDo: Remove Double Guard statement if (heldTime <= MaxJumpTime && Vector2.Dot(_rb.velocity, Vector2.up) > 0) { float frameRatio = heldTime / MaxJumpTime; frameRatio = Mathf.Abs(frameRatio - 1); //Debug.Log(frameRatio); //float yForce = Mathf.Lerp(m_jumpForce + 2, 0, frameRatio); _rb.AddForce(m_jumpForce * 1.025f * 2 * _rb.mass * Vector2.up * frameRatio); } else { jumpInput = false; } } public void SetJumpForceAtDesiredHeight() { m_jumpForce = Mathf.Sqrt(_rb.gravityScale * Physics2D.gravity.magnitude * 2 * jumpHeight); } public void ChangeState(NecroBaseState state) { //Guard Statement if (state == m_currentState) { return; } m_currentState = state; m_currentState.EnterState(this); } public static float Approach(float val, float target, float maxMove) { return val > target ? Mathf.Max(val - maxMove, target) : Mathf.Min(val + maxMove, target); } public void TryToDie() { if (invincibleFrameTimer <= 0) { Death(); } } public void Death() { ChangeState(m_necroDeadState); SFXManager.instance.Play("Splat"); } //ToDo: Move CapsuleMorph to another script public void MorphCapsuleColliderVertical() { if (_capsuleCollider.direction == CapsuleDirection2D.Vertical) { return; } _capsuleCollider.direction = CapsuleDirection2D.Vertical; _capsuleCollider.offset = Vector2.zero; DOTween.To(() => _capsuleCollider.size, x => _capsuleCollider.size = x, initialVerticalCapsuleSize, .45f).SetEase(Ease.InOutCubic); } public void Revive() { invincibleFrameTimer = invincibleTime;//This can also be set when we enter the death state ReturnToCorrectState(); } public void ReturnToCorrectState() { if (_collisionHelper.onGround) { ChangeState(GroundedState); } else { ChangeState(FallingState); } } public void MorphCapsuleColliderHorizontal() { if (_capsuleCollider.direction == CapsuleDirection2D.Horizontal) { return; } _capsuleCollider.direction = CapsuleDirection2D.Horizontal; _capsuleCollider.offset = capsuleColliderDeadOffset; DOTween.To(() => _capsuleCollider.size, x => _capsuleCollider.size = x, new Vector2(initialVerticalCapsuleSize.y, initialVerticalCapsuleSize.x), .45f).SetEase(Ease.InOutCubic)/*.OnComplete(Hop)*/; } private void OnTriggerEnter2D(Collider2D collision) { TryPopulateInteractable(collision); } private void OnTriggerExit2D(Collider2D collision) { TryDisposeInteractable(collision); } private void TryPopulateInteractable(Collider2D collision) { var other = collision.GetComponent(); if (!ReferenceEquals(other, null) && ReferenceEquals(_interactable, null)) { _interactable = other; } } private void TryDisposeInteractable(Collider2D collision) { var other = collision.GetComponent(); if (!ReferenceEquals(other, _interactable)) { DisposeInteractable(); } } public void DisposeInteractable() { _interactable = null; } private void OnValidate() { if (Application.isPlaying) { return; } if (_collisionHelper == null) { _collisionHelper = GetComponent(); } if (_rb == null) { _rb = GetComponent(); } } }