Files
Colosseum/Assets/_Game/Scripts/Player/HitReactionController.cs
dal4segno 35a5b272cb feat: 경직 면역 기반 시전 보호 및 지원 스킬 안정성 보강
- 경직 면역 이상상태와 시전 시작 효과를 추가해 철벽, 방어 태세, 치유, 광역 치유, 보호막에 데이터 기반 시전 보호를 연결
- AbnormalityManager와 HitReactionController가 경직 면역 상태를 존중하도록 보강해 일반 피격 반응으로 인한 즉시 취소를 줄임
- SkillData에 castStartEffects를 추가하고 SkillController가 시전 시작 효과를 실행하도록 확장
- 드로그전 재검증에서 철벽, 치유, 광역 치유가 실제 전투 중 취소 없이 완료되는 것을 확인하고 보호막의 후속 피격 체감을 추가 점검 대상으로 정리
- HUD/문서 반영 과정에서 필요한 TMP_MaruBuri, TMP_SuseongBatang 아틀라스 갱신을 함께 포함
2026-03-25 02:47:27 +09:00

230 lines
6.7 KiB
C#

using UnityEngine;
using Unity.Netcode;
using Colosseum.Abnormalities;
using Colosseum.Skills;
namespace Colosseum.Player
{
/// <summary>
/// 플레이어의 피격 제어 상태를 관리합니다.
/// 넉백 강제 이동과 다운 상태, 피격 애니메이션 재생을 담당합니다.
/// </summary>
[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 downTriggerParam = "Down";
[Tooltip("기상 트리거 이름")]
[SerializeField] private string recoverTriggerParam = "Recover";
[Header("Settings")]
[Tooltip("애니메이션 파라미터가 없을 때 경고 로그 출력")]
[SerializeField] private bool logMissingAnimationParams = false;
private readonly NetworkVariable<bool> isDowned = new NetworkVariable<bool>(false);
private float downRemainingTime;
/// <summary>
/// 다운 상태 여부
/// </summary>
public bool IsDowned => isDowned.Value;
/// <summary>
/// 넉백 강제 이동 진행 여부
/// </summary>
public bool IsKnockbackActive => playerMovement != null && playerMovement.IsForcedMoving;
/// <summary>
/// 피격 반응 무시 상태 여부
/// </summary>
public bool IsHitReactionImmune => abnormalityManager != null && abnormalityManager.IsHitReactionImmune;
private void Awake()
{
ResolveReferences();
}
public override void OnNetworkSpawn()
{
ResolveReferences();
}
private void Update()
{
if (!IsServer || !isDowned.Value)
return;
downRemainingTime -= Time.deltaTime;
if (downRemainingTime <= 0f)
{
RecoverFromDown();
}
}
/// <summary>
/// 넉백을 적용합니다.
/// </summary>
public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
if (IsHitReactionImmune)
return;
playerMovement?.ApplyForcedMovement(worldVelocity, duration);
if (playHitAnimation)
{
TriggerAnimationRpc(hitTriggerParam);
}
}
/// <summary>
/// 다운 상태를 적용합니다.
/// </summary>
public void ApplyDown(float duration)
{
if (!IsServer)
return;
ResolveReferences();
if (networkController != null && networkController.IsDead)
return;
if (IsHitReactionImmune)
return;
downRemainingTime = Mathf.Max(downRemainingTime, duration);
if (isDowned.Value)
return;
isDowned.Value = true;
skillController?.CancelSkill(SkillCancelReason.HitReaction);
playerMovement?.ClearForcedMovement();
TriggerAnimationRpc(downTriggerParam);
}
/// <summary>
/// 다운 상태를 해제합니다.
/// </summary>
public void RecoverFromDown()
{
if (!IsServer || !isDowned.Value)
return;
isDowned.Value = false;
downRemainingTime = 0f;
TriggerAnimationRpc(recoverTriggerParam);
}
/// <summary>
/// 피격 상태를 즉시 초기화합니다.
/// </summary>
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<PlayerMovement>();
if (skillController == null)
skillController = GetComponent<SkillController>();
if (networkController == null)
networkController = GetComponent<PlayerNetworkController>();
if (abnormalityManager == null)
abnormalityManager = GetComponent<AbnormalityManager>();
if (animator == null)
animator = GetComponentInChildren<Animator>();
}
}
}