Files
tiny_tackle_heroes/Assets/_Project/Scripts/Player/PlayerController.cs
T

472 lines
17 KiB
C#

using UnityEngine;
using UnityEngine.InputSystem;
using BumpCombat.Core;
using BumpCombat.Constants;
using BumpCombat.Progression;
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");
private static readonly int DeathState =
Animator.StringToHash("Base Layer.Death");
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 PassiveRelicController passiveRelics;
private Vector2 moveInput;
private Vector2 pendingImpactRecoil;
private Vector2 damageKnockbackDirection;
private float damageKnockbackDistance;
private float damageKnockbackDuration;
private float damageKnockbackElapsed;
private Vector2 voluntaryMovementStart;
private bool voluntaryMovementPending;
public void SetPassiveRelicController(PassiveRelicController relics) => passiveRelics = relics;
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 bool IsTransitionLocked { get; private set; }
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<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
animator = GetComponent<Animator>();
playerStats = GetComponent<PlayerStats>();
dashController = GetComponent<DashController>();
activeArtifactController = GetComponent<ActiveArtifactController>();
playerHealth = GetComponent<PlayerHealth>();
passiveRelics = GetComponent<PassiveRelicController>();
}
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);
}
public float PlayDeathAnimation()
{
if (animator == null
|| animator.runtimeAnimatorController == null
|| !animator.isActiveAndEnabled
|| !animator.HasState(0, DeathState))
{
return 0f;
}
SetTransitionLocked(true);
animator.SetBool(IsMovingParameter, false);
animator.Play(DeathState, 0, 0f);
animator.Update(0f);
return Mathf.Max(0f, animator.GetCurrentAnimatorStateInfo(0).length);
}
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 || IsTransitionLocked)
{
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()
{
if (voluntaryMovementPending
&& body != null
&& !IsTransitionLocked
&& !IsDamageKnockbackActive
&& (dashController == null || !dashController.IsDashing))
{
passiveRelics?.AddVoluntaryMovement(
Vector2.Distance(voluntaryMovementStart, body.position));
}
voluntaryMovementPending = false;
AttackIntentVelocity = Vector2.zero;
if (IsTransitionLocked)
{
if (body != null) body.linearVelocity = Vector2.zero;
CurrentVelocity = Vector2.zero;
return;
}
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);
voluntaryMovementStart = startPosition;
voluntaryMovementPending = targetPosition != startPosition;
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 Vector2 ClampMovementToTerrain(Vector2 start, Vector2 desiredPosition)
{
Vector2 desired = ClampToArena(desiredPosition);
Vector2 delta = desired - start;
float distance = delta.magnitude;
if (distance <= 0.0001f)
{
return desired;
}
Collider2D ownCollider = GetComponent<Collider2D>();
float radius = 0.2f;
if (ownCollider != null)
{
Vector2 extents = ownCollider.bounds.extents;
radius = Mathf.Max(0.05f, Mathf.Min(extents.x, extents.y) * 0.9f);
}
Vector2 direction = delta / distance;
RaycastHit2D[] hits = Physics2D.CircleCastAll(
start,
radius,
direction,
distance,
Physics2D.AllLayers);
float allowedDistance = distance;
for (int i = 0; i < hits.Length; i++)
{
Collider2D collider = hits[i].collider;
if (collider == null
|| collider.isTrigger
|| collider == ownCollider
|| collider.transform.IsChildOf(transform))
{
continue;
}
bool isTerrain = collider.GetComponentInParent<BumpCombat.Stage.StageObstacle>() != null
|| collider.name.StartsWith("Wall_")
|| collider.gameObject.name.Contains("Wall");
if (isTerrain)
{
allowedDistance = Mathf.Min(
allowedDistance,
Mathf.Max(0f, hits[i].distance - 0.02f));
}
}
return start + direction * allowedDistance;
}
public bool TryReposition(Vector2 position)
{
if (body == null)
{
return false;
}
pendingImpactRecoil = Vector2.zero;
IsDamageKnockbackActive = false;
body.linearVelocity = Vector2.zero;
voluntaryMovementPending = false;
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;
}
}
public void SetTransitionLocked(bool locked)
{
IsTransitionLocked = locked;
if (locked)
{
moveInput = Vector2.zero;
CurrentVelocity = Vector2.zero;
AttackIntentVelocity = Vector2.zero;
if (body != null)
{
body.linearVelocity = Vector2.zero;
}
if (animator != null && animator.isActiveAndEnabled)
{
animator.SetBool(IsMovingParameter, false);
}
}
}
public void SetFacingDirection(Vector2 direction)
{
if (direction.sqrMagnitude > 0.0001f)
{
FacingDirection = direction.normalized;
if (spriteRenderer != null && !Mathf.Approximately(direction.x, 0f))
{
spriteRenderer.flipX = direction.x < 0f;
}
}
}
}
}