using UnityEngine; using Unity.Netcode; 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("피격 애니메이션을 재생할 Animator")] [SerializeField] private Animator animator; [Header("Animation")] [Tooltip("일반 피격 트리거 이름")] [SerializeField] private string hitTriggerParam = "Hit"; [Tooltip("다운 시작 트리거 이름")] [SerializeField] private string downTriggerParam = "Down"; [Tooltip("기상 트리거 이름")] [SerializeField] private string recoverTriggerParam = "Recover"; [Header("Settings")] [Tooltip("애니메이션 파라미터가 없을 때 경고 로그 출력")] [SerializeField] private bool logMissingAnimationParams = false; private readonly NetworkVariable isDowned = new NetworkVariable(false); private float downRemainingTime; /// /// 다운 상태 여부 /// public bool IsDowned => isDowned.Value; /// /// 넉백 강제 이동 진행 여부 /// public bool IsKnockbackActive => playerMovement != null && playerMovement.IsForcedMoving; private void Awake() { ResolveReferences(); } public override void OnNetworkSpawn() { ResolveReferences(); } private void Update() { if (!IsServer || !isDowned.Value) return; downRemainingTime -= Time.deltaTime; if (downRemainingTime <= 0f) { RecoverFromDown(); } } /// /// 넉백을 적용합니다. /// public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true) { if (!IsServer) return; ResolveReferences(); if (networkController != null && networkController.IsDead) return; playerMovement?.ApplyForcedMovement(worldVelocity, duration); if (playHitAnimation) { TriggerAnimationRpc(hitTriggerParam); } } /// /// 다운 상태를 적용합니다. /// public void ApplyDown(float duration) { if (!IsServer) return; ResolveReferences(); if (networkController != null && networkController.IsDead) return; downRemainingTime = Mathf.Max(downRemainingTime, duration); if (isDowned.Value) return; isDowned.Value = true; skillController?.CancelSkill(SkillCancelReason.HitReaction); playerMovement?.ClearForcedMovement(); TriggerAnimationRpc(downTriggerParam); } /// /// 다운 상태를 해제합니다. /// public void RecoverFromDown() { if (!IsServer || !isDowned.Value) return; isDowned.Value = false; downRemainingTime = 0f; TriggerAnimationRpc(recoverTriggerParam); } /// /// 피격 상태를 즉시 초기화합니다. /// public void ClearHitReactionState(bool playRecoverAnimation = false) { if (!IsServer) return; ResolveReferences(); playerMovement?.ClearForcedMovement(); if (!isDowned.Value) return; isDowned.Value = false; downRemainingTime = 0f; if (playRecoverAnimation) { TriggerAnimationRpc(recoverTriggerParam); } } [Rpc(SendTo.Everyone)] private void TriggerAnimationRpc(string triggerName) { ResolveReferences(); if (animator == null || string.IsNullOrWhiteSpace(triggerName)) return; if (!HasTrigger(triggerName)) { if (logMissingAnimationParams) { Debug.LogWarning($"[HitReaction] Animator trigger not found: {triggerName}"); } return; } animator.SetTrigger(triggerName); } 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 (animator == null) animator = GetComponentInChildren(); } } }