using UnityEngine;
using Unity.Netcode;
using Colosseum.Abnormalities;
using Colosseum.Skills;
namespace Colosseum.Player
{
///
/// 플레이어의 피격 제어 상태를 관리합니다.
/// 경직, 넉백, 다운 상태와 피격 애니메이션 재생을 담당합니다.
///
[DisallowMultipleComponent]
[RequireComponent(typeof(PlayerMovement))]
public class HitReactionController : NetworkBehaviour
{
[Header("References")]
[Tooltip("플레이어 이동 컴포넌트")]
[SerializeField] private PlayerMovement playerMovement;
[Tooltip("스킬 실행 관리자")]
[SerializeField] private SkillController skillController;
[Tooltip("플레이어 네트워크 상태")]
[SerializeField] private PlayerNetworkController networkController;
[Tooltip("이상상태 관리자")]
[SerializeField] private AbnormalityManager abnormalityManager;
[Tooltip("피격 애니메이션을 재생할 Animator")]
[SerializeField] private Animator animator;
[Header("Animation")]
[Tooltip("일반 피격 트리거 이름")]
[SerializeField] private string hitTriggerParam = "Hit";
[Tooltip("일반 피격 애니메이션 속도 배율 파라미터 이름")]
[SerializeField] private string hitSpeedMultiplierParam = "HitSpeedMultiplier";
[Tooltip("다운 시작 트리거 이름")]
[SerializeField] private string downTriggerParam = "Down";
[Tooltip("기상 트리거 이름")]
[SerializeField] private string recoverTriggerParam = "Recover";
[Header("Settings")]
[Tooltip("DownBegin 종료 후 구르기 가능 구간까지 대기 시간")]
[Min(0f)] [SerializeField] private float downRecoverableDelayAfterBeginExit = 0.2f;
[Tooltip("애니메이션 파라미터가 없을 때 경고 로그 출력")]
[SerializeField] private bool logMissingAnimationParams = false;
private readonly NetworkVariable isDowned = new NetworkVariable(false);
private readonly NetworkVariable isDownRecoverable = new NetworkVariable(false);
private readonly NetworkVariable isKnockbackActive = new NetworkVariable(false);
private readonly NetworkVariable isStaggered = new NetworkVariable(false);
private float downRemainingTime;
private float downRecoverableDelayRemaining = -1f;
private float knockbackRemainingTime;
private float staggerRemainingTime;
private bool isDownRecoveryAnimating;
private bool isDownLoopTimingActive;
///
/// 다운 상태 여부
///
public bool IsDowned => isDowned.Value;
///
/// 다운 중 구르기 가능 구간 여부
///
public bool IsDownRecoverable => isDownRecoverable.Value;
///
/// 넉백 강제 이동 진행 여부
///
public bool IsKnockbackActive => isKnockbackActive.Value;
///
/// 경직 상태 여부
///
public bool IsStaggered => isStaggered.Value;
///
/// 경직 면역 상태 여부
///
public bool IsStaggerImmune => abnormalityManager != null && abnormalityManager.IsStaggerImmune;
///
/// 넉백 면역 상태 여부
///
public bool IsKnockbackImmune => abnormalityManager != null && abnormalityManager.IsKnockbackImmune;
///
/// 다운 면역 상태 여부
///
public bool IsDownImmune => abnormalityManager != null && abnormalityManager.IsDownImmune;
private void Awake()
{
ResolveReferences();
}
public override void OnNetworkSpawn()
{
ResolveReferences();
}
private void Update()
{
if (!IsServer)
return;
UpdateKnockbackState(Time.deltaTime);
UpdateStaggerState(Time.deltaTime);
UpdateDownState(Time.deltaTime);
}
///
/// 경직을 적용합니다.
///
public void ApplyStagger(float duration, bool playHitAnimation = true, float hitAnimationSpeedMultiplier = 1f)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
if (IsStaggerImmune || isDowned.Value)
return;
if (duration <= 0f)
{
ClearStaggerState();
return;
}
staggerRemainingTime = Mathf.Max(staggerRemainingTime, duration);
isStaggered.Value = true;
skillController?.CancelSkill(SkillCancelReason.Stagger);
if (playHitAnimation)
{
TriggerAnimationRpc(hitTriggerParam, hitAnimationSpeedMultiplier);
}
}
///
/// 넉백을 적용합니다.
///
public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true, float hitAnimationSpeedMultiplier = 1f)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
if (IsKnockbackImmune || isDowned.Value)
return;
if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f)
{
ClearKnockbackState();
playerMovement?.ClearForcedMovement();
return;
}
knockbackRemainingTime = Mathf.Max(knockbackRemainingTime, duration);
isKnockbackActive.Value = true;
skillController?.CancelSkill(SkillCancelReason.HitReaction);
playerMovement?.ApplyForcedMovement(worldVelocity, duration);
if (playHitAnimation)
{
TriggerAnimationRpc(hitTriggerParam, hitAnimationSpeedMultiplier);
}
}
///
/// 다운 상태를 적용합니다.
///
public void ApplyDown(float duration)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
if (IsDownImmune)
return;
downRemainingTime = Mathf.Max(downRemainingTime, duration);
if (isDowned.Value)
return;
isDowned.Value = true;
isDownRecoverable.Value = false;
isDownRecoveryAnimating = false;
isDownLoopTimingActive = false;
downRecoverableDelayRemaining = -1f;
ClearKnockbackState();
ClearStaggerState();
skillController?.CancelSkill(SkillCancelReason.HitReaction);
playerMovement?.ClearForcedMovement();
TriggerAnimationRpc(downTriggerParam);
}
///
/// DownBegin 종료 시점을 전달받아 구르기 가능 타이머를 시작합니다.
///
public void NotifyDownBeginExited()
{
if (!IsServer || !isDowned.Value || isDownRecoveryAnimating)
return;
isDownLoopTimingActive = true;
downRecoverableDelayRemaining = downRecoverableDelayAfterBeginExit;
}
///
/// Recover 상태가 끝났을 때 다운 상태를 최종 해제합니다.
///
public void NotifyDownRecoverAnimationExited()
{
if (!IsServer || !isDowned.Value || !isDownRecoveryAnimating)
return;
ClearDownState();
}
///
/// 다운 회복 가능 구간에서 구르기를 사용하며 다운 상태를 종료합니다.
///
public bool TryConsumeDownRecoverableEvade()
{
if (!IsServer || !isDowned.Value || !isDownRecoverable.Value)
return false;
ClearDownState();
return true;
}
///
/// 피격 상태를 즉시 초기화합니다.
///
public void ClearHitReactionState(bool playRecoverAnimation = false)
{
if (!IsServer)
return;
ResolveReferences();
playerMovement?.ClearForcedMovement();
ClearKnockbackState();
ClearStaggerState();
if (!isDowned.Value)
return;
ClearDownState();
if (playRecoverAnimation)
{
TriggerAnimationRpc(recoverTriggerParam);
}
}
[Rpc(SendTo.Everyone)]
private void TriggerAnimationRpc(string triggerName, float hitAnimationSpeedMultiplier = 1f)
{
ResolveReferences();
if (animator == null || string.IsNullOrWhiteSpace(triggerName))
return;
if (triggerName == hitTriggerParam)
{
SetFloatParameterIfExists(hitSpeedMultiplierParam, Mathf.Max(0.01f, hitAnimationSpeedMultiplier));
}
if (!HasTrigger(triggerName))
{
if (logMissingAnimationParams)
{
Debug.LogWarning($"[HitReaction] Animator trigger not found: {triggerName}");
}
return;
}
animator.SetTrigger(triggerName);
}
private void SetFloatParameterIfExists(string parameterName, float value)
{
if (animator == null || string.IsNullOrWhiteSpace(parameterName))
return;
for (int i = 0; i < animator.parameterCount; i++)
{
AnimatorControllerParameter parameter = animator.GetParameter(i);
if (parameter.type == AnimatorControllerParameterType.Float && parameter.name == parameterName)
{
animator.SetFloat(parameterName, value);
return;
}
}
if (logMissingAnimationParams)
{
Debug.LogWarning($"[HitReaction] Animator float parameter not found: {parameterName}");
}
}
private bool HasTrigger(string triggerName)
{
if (animator == null || string.IsNullOrWhiteSpace(triggerName))
return false;
for (int i = 0; i < animator.parameterCount; i++)
{
AnimatorControllerParameter parameter = animator.GetParameter(i);
if (parameter.type == AnimatorControllerParameterType.Trigger && parameter.name == triggerName)
return true;
}
return false;
}
private void ResolveReferences()
{
if (playerMovement == null)
playerMovement = GetComponent();
if (skillController == null)
skillController = GetComponent();
if (networkController == null)
networkController = GetComponent();
if (abnormalityManager == null)
abnormalityManager = GetComponent();
if (animator == null)
animator = GetComponentInChildren();
}
private void UpdateKnockbackState(float deltaTime)
{
if (!isKnockbackActive.Value)
return;
knockbackRemainingTime -= deltaTime;
if (knockbackRemainingTime <= 0f)
{
ClearKnockbackState();
}
}
private void UpdateStaggerState(float deltaTime)
{
if (!isStaggered.Value)
return;
staggerRemainingTime -= deltaTime;
if (staggerRemainingTime <= 0f)
{
ClearStaggerState();
}
}
private void UpdateDownState(float deltaTime)
{
if (!isDowned.Value)
return;
if (isDownLoopTimingActive)
{
downRemainingTime -= deltaTime;
}
if (!isDownRecoverable.Value && downRecoverableDelayRemaining >= 0f)
{
downRecoverableDelayRemaining -= deltaTime;
if (downRecoverableDelayRemaining <= 0f)
{
EnterDownRecoverableState();
}
}
if (downRemainingTime <= 0f)
{
BeginDownRecoveryAnimation();
}
}
private void EnterDownRecoverableState()
{
if (!isDowned.Value)
return;
isDownRecoverable.Value = true;
downRecoverableDelayRemaining = -1f;
}
private void BeginDownRecoveryAnimation()
{
if (!isDowned.Value || isDownRecoveryAnimating)
return;
EnterDownRecoverableState();
isDownRecoveryAnimating = true;
isDownLoopTimingActive = false;
downRemainingTime = 0f;
TriggerAnimationRpc(recoverTriggerParam);
}
private void ClearDownState()
{
isDowned.Value = false;
isDownRecoverable.Value = false;
isDownRecoveryAnimating = false;
isDownLoopTimingActive = false;
downRemainingTime = 0f;
downRecoverableDelayRemaining = -1f;
}
private void ClearKnockbackState()
{
isKnockbackActive.Value = false;
knockbackRemainingTime = 0f;
}
private void ClearStaggerState()
{
isStaggered.Value = false;
staggerRemainingTime = 0f;
}
}
}