using UnityEngine; using UnityEngine.InputSystem; using BumpCombat.Core; using BumpCombat.Constants; namespace BumpCombat.Player { [RequireComponent(typeof(Rigidbody2D), typeof(SpriteRenderer), typeof(Animator))] [RequireComponent(typeof(PlayerStats))] public sealed class PlayerController : MonoBehaviour { private static readonly int IsMovingParameter = Animator.StringToHash("IsMoving"); private static readonly int BumpAttackState = Animator.StringToHash("Base Layer.BumpAttack"); private static readonly int BackBumpAttackState = Animator.StringToHash("Base Layer.BackBumpAttack"); private static readonly int ArtifactUseState = Animator.StringToHash("Base Layer.ArtifactUse"); public const string BumpAttackStateName = "BumpAttack"; public const string BackBumpAttackStateName = "BackBumpAttack"; public const string ArtifactUseStateName = "ArtifactUse"; public const string HurtStateName = "Hurt"; public const string DashStateName = "Dash"; public const string DeathStateName = "Death"; private Rigidbody2D body; private SpriteRenderer spriteRenderer; private Animator animator; private PlayerStats playerStats; private DashController dashController; private ActiveArtifactController activeArtifactController; private PlayerHealth playerHealth; private Vector2 moveInput; private Vector2 pendingImpactRecoil; private Vector2 damageKnockbackDirection; private float damageKnockbackDistance; private float damageKnockbackDuration; private float damageKnockbackElapsed; public Vector2 MoveDirection { get; private set; } public Vector2 FacingDirection { get; private set; } = Vector2.right; public Vector2 CurrentVelocity { get; private set; } public Vector2 AttackIntentVelocity { get; private set; } public bool IsDamageKnockbackActive { get; private set; } public bool IsHurtMovementLocked => playerHealth != null && playerHealth.IsHurtMovementLocked; public Vector2 PlayerClampHalfExtents { get { ArenaBounds bounds = ArenaBounds.Resolve(); return bounds != null ? bounds.PlayerClampHalfExtents : new Vector2( Mathf.Max(0f, GameplayConstants.Current.Player.ArenaHalfExtents.x - GameplayConstants.Current.Player.ArenaPadding.x), Mathf.Max(0f, GameplayConstants.Current.Player.ArenaHalfExtents.y - GameplayConstants.Current.Player.ArenaPadding.y)); } } private void Awake() { body = GetComponent(); spriteRenderer = GetComponent(); animator = GetComponent(); playerStats = GetComponent(); dashController = GetComponent(); activeArtifactController = GetComponent(); playerHealth = GetComponent(); } private void OnDisable() { if (animator == null || animator.runtimeAnimatorController == null || !animator.isActiveAndEnabled) { return; } animator.SetBool(IsMovingParameter, false); animator.Rebind(); animator.Update(0f); } public bool TryPlayBumpAttackAnimation(bool isBackHit = false) { return TryPlayCombatAnimation(isBackHit ? BackBumpAttackState : BumpAttackState, true); } public bool TryPlayArtifactUseAnimation() { return TryPlayCombatAnimation(ArtifactUseState, false); } private bool TryPlayCombatAnimation( int stateHash, bool blockActiveArtifactUse) { if (animator == null || animator.runtimeAnimatorController == null || !animator.isActiveAndEnabled || !animator.HasState(0, stateHash)) { return false; } AnimatorStateInfo current = animator.GetCurrentAnimatorStateInfo(0); AnimatorStateInfo next = animator.IsInTransition(0) ? animator.GetNextAnimatorStateInfo(0) : default; bool priorityStateActive = IsPriorityAnimationState(current) || IsPriorityAnimationState(next); bool artifactUseStateActive = blockActiveArtifactUse && (IsAnimationState(current, ArtifactUseStateName) || IsAnimationState(next, ArtifactUseStateName)); bool isDead = playerHealth != null && playerHealth.CurrentHealth <= 0f; bool isDashing = dashController != null && dashController.IsDashing; if (isDead || IsHurtMovementLocked || isDashing || priorityStateActive || artifactUseStateActive) { return false; } animator.Play(stateHash, 0, 0f); return true; } private static bool IsPriorityAnimationState(AnimatorStateInfo state) { return IsAnimationState(state, HurtStateName) || IsAnimationState(state, DashStateName) || IsAnimationState(state, DeathStateName); } private static bool IsAnimationState( AnimatorStateInfo state, string stateName) { return state.IsName(stateName); } private void Update() { if (!RunManager.GameplayInputEnabled || IsHurtMovementLocked) { SetMoveInput(Vector2.zero); return; } Keyboard keyboard = Keyboard.current; if (keyboard == null) { SetMoveInput(Vector2.zero); return; } float horizontal = (keyboard.rightArrowKey.isPressed ? 1f : 0f) - (keyboard.leftArrowKey.isPressed ? 1f : 0f); float vertical = (keyboard.upArrowKey.isPressed ? 1f : 0f) - (keyboard.downArrowKey.isPressed ? 1f : 0f); SetMoveInput(Vector2.ClampMagnitude(new Vector2(horizontal, vertical), 1f)); } private void FixedUpdate() { AttackIntentVelocity = Vector2.zero; if (dashController != null && dashController.IsDashing) { pendingImpactRecoil = Vector2.zero; IsDamageKnockbackActive = false; CurrentVelocity = Vector2.zero; return; } Vector2 startPosition = body.position; if (IsDamageKnockbackActive) { UpdateDamageKnockback(); return; } if (activeArtifactController != null && activeArtifactController.LocksMovement) { body.linearVelocity = Vector2.zero; CurrentVelocity = Vector2.zero; return; } if (IsHurtMovementLocked) { body.linearVelocity = Vector2.zero; CurrentVelocity = Vector2.zero; return; } if (pendingImpactRecoil.sqrMagnitude > 0f) { Vector2 recoilTarget = ClampToArena(startPosition + pendingImpactRecoil); pendingImpactRecoil = Vector2.zero; body.linearVelocity = Vector2.zero; body.MovePosition(recoilTarget); CurrentVelocity = Vector2.zero; return; } AttackIntentVelocity = moveInput * playerStats.MoveSpeed; Vector2 targetPosition = ClampToArena( startPosition + AttackIntentVelocity * Time.fixedDeltaTime); body.MovePosition(targetPosition); CurrentVelocity = (targetPosition - startPosition) / Time.fixedDeltaTime; if (CurrentVelocity.sqrMagnitude > 0.0001f) { activeArtifactController?.AddMovementCharge(Time.fixedDeltaTime); } } public Vector2 ClampToArena(Vector2 position) { ArenaBounds bounds = ArenaBounds.Resolve(); return bounds != null ? bounds.ClampPlayerPosition(position) : new Vector2( Mathf.Clamp( position.x, -GameplayConstants.Current.Player.ArenaHalfExtents.x + GameplayConstants.Current.Player.ArenaPadding.x, GameplayConstants.Current.Player.ArenaHalfExtents.x - GameplayConstants.Current.Player.ArenaPadding.x), Mathf.Clamp( position.y, -GameplayConstants.Current.Player.ArenaHalfExtents.y + GameplayConstants.Current.Player.ArenaPadding.y, GameplayConstants.Current.Player.ArenaHalfExtents.y - GameplayConstants.Current.Player.ArenaPadding.y)); } public bool TryReposition(Vector2 position) { if (body == null) { return false; } pendingImpactRecoil = Vector2.zero; IsDamageKnockbackActive = false; body.linearVelocity = Vector2.zero; body.position = ClampToArena(position); transform.position = body.position; return true; } public void ApplyImpactRecoil(Vector2 direction, float distance) { if (direction.sqrMagnitude <= 0f || distance <= 0f) { return; } Vector2 recoil = direction.normalized * distance; pendingImpactRecoil = Vector2.ClampMagnitude( pendingImpactRecoil + recoil, distance); } public void ApplyDamageKnockback(Vector2 direction, float distance, float duration) { if (direction.sqrMagnitude <= 0f || distance <= 0f) { return; } pendingImpactRecoil = Vector2.zero; body.linearVelocity = Vector2.zero; damageKnockbackDirection = direction.normalized; damageKnockbackDistance = distance; damageKnockbackDuration = Mathf.Max(duration, Time.fixedDeltaTime); damageKnockbackElapsed = 0f; IsDamageKnockbackActive = true; CurrentVelocity = Vector2.zero; } private void UpdateDamageKnockback() { float previousProgress = Mathf.Clamp01( damageKnockbackElapsed / damageKnockbackDuration); damageKnockbackElapsed = Mathf.Min( damageKnockbackElapsed + Time.fixedDeltaTime, damageKnockbackDuration); float currentProgress = Mathf.Clamp01( damageKnockbackElapsed / damageKnockbackDuration); float previousEase = 1f - (1f - previousProgress) * (1f - previousProgress); float currentEase = 1f - (1f - currentProgress) * (1f - currentProgress); float stepDistance = damageKnockbackDistance * (currentEase - previousEase); body.linearVelocity = Vector2.zero; body.MovePosition(ClampToArena( body.position + damageKnockbackDirection * stepDistance)); CurrentVelocity = Vector2.zero; if (damageKnockbackElapsed >= damageKnockbackDuration) { IsDamageKnockbackActive = false; } } private void SetMoveInput(Vector2 input) { moveInput = input; MoveDirection = input; if (IsDamageKnockbackActive || IsHurtMovementLocked || (activeArtifactController != null && activeArtifactController.LocksMovement)) { animator.SetBool(IsMovingParameter, false); return; } animator.SetBool(IsMovingParameter, input.sqrMagnitude > 0f); if (input.sqrMagnitude <= 0f) { return; } FacingDirection = input.normalized; if (!Mathf.Approximately(input.x, 0f)) { spriteRenderer.flipX = input.x < 0f; } } } }