using System; using UnityEngine; using Colosseum.Combat; using Colosseum.Skills; namespace Colosseum.Player { /// /// 플레이어의 방어 판정 상태를 관리합니다. /// 마나 유지나 이동 감속 없이 순수하게 방어 가능 여부와 피해 감쇠만 처리합니다. /// [DisallowMultipleComponent] public class PlayerDefenseController : MonoBehaviour { [Header("References")] [Tooltip("완벽한 방어 보상과 이동 감속을 처리하는 유지 컨트롤러")] [SerializeField] private PlayerDefenseSustainController sustainController; [Header("Defense Settings")] [Tooltip("정면 판정을 사용할지 여부")] [SerializeField] private bool useFrontGuardArc = true; [Tooltip("방어가 유효한 정면 반각입니다.")] [Range(1f, 89f)] [SerializeField] private float guardHalfAngle = 75f; [Tooltip("일반 방어 성공 시 남는 피해 배율입니다.")] [Range(0f, 1f)] [SerializeField] private float guardDamageMultiplier = 0.65f; [Tooltip("방어 시작 후 완벽한 방어로 인정하는 시간 창입니다.")] [Min(0f)] [SerializeField] private float perfectGuardWindow = 0.5f; [Tooltip("방어 판정 상세 로그 출력 여부")] [SerializeField] private bool enableDefenseDebugLogs = true; [Header("Debug")] [Tooltip("현재 방어 판정 활성 여부")] [SerializeField] private bool isDefenseStateActive; [Tooltip("현재 방어 판정 유지 시간")] [Min(0f)] [SerializeField] private float defenseStateElapsed; [Tooltip("마지막 방어 판정으로 막은 피해량")] [Min(0f)] [SerializeField] private float lastPreventedDamage; [Tooltip("마지막 방어 판정이 완벽한 방어였는지 여부")] [SerializeField] private bool lastWasPerfectGuard; private bool perfectGuardAvailable; /// /// 방어 시작 시 완벽한 방어 유효 시간을 전달합니다. /// public event Action OnDefenseStateEntered; /// /// 방어 성공 시 일반/완벽 여부와 막은 피해량을 전달합니다. /// public event Action OnDefenseResolved; /// /// 현재 방어 판정 활성 여부입니다. /// public bool IsDefenseStateActive => isDefenseStateActive; /// /// 방어 유지 시스템이 제공하는 이동 속도 배율입니다. /// public float MoveSpeedMultiplier { get { EnsureReferences(); return sustainController != null ? sustainController.MoveSpeedMultiplier : 1f; } } private void Awake() { EnsureReferences(); } private void OnDisable() { ClearDefenseState(); } private void Update() { if (!isDefenseStateActive) return; defenseStateElapsed += Time.deltaTime; } /// /// 애니메이션 이벤트로 방어 판정을 시작합니다. /// public void EnterDefenseState() { EnsureReferences(); isDefenseStateActive = true; defenseStateElapsed = 0f; lastPreventedDamage = 0f; lastWasPerfectGuard = false; perfectGuardAvailable = true; OnDefenseStateEntered?.Invoke(perfectGuardWindow); if (enableDefenseDebugLogs) { Debug.Log($"[Defense] 상태 시작 | owner={gameObject.name} | perfectWindow={perfectGuardWindow:F2}s"); } } /// /// 애니메이션 이벤트로 방어 판정을 종료합니다. /// public void ExitDefenseState() { ClearDefenseState(); } /// /// 스킬 종료/취소 시 방어 판정을 정리합니다. /// public void HandleSkillExecutionEnded() { ClearDefenseState(); } /// /// 현재 방어 판정이 유효하다면 피해를 감쇠합니다. /// public float ResolveIncomingDamage(DamageContext damageContext) { EnsureReferences(); if (!isDefenseStateActive || !damageContext.CanBeGuarded) return damageContext.Amount; if (useFrontGuardArc && !IsWithinGuardArc(damageContext.SourceGameObject)) return damageContext.Amount; bool isPerfectGuard = perfectGuardAvailable && defenseStateElapsed <= perfectGuardWindow; perfectGuardAvailable = false; lastWasPerfectGuard = isPerfectGuard; if (isPerfectGuard) { sustainController?.RefundStartManaOnPerfectGuard(); lastPreventedDamage = damageContext.Amount; LogDefenseResolution(damageContext, true, 0f); OnDefenseResolved?.Invoke(true, lastPreventedDamage); return 0f; } float resolvedDamage = damageContext.Amount * guardDamageMultiplier; lastPreventedDamage = Mathf.Max(0f, damageContext.Amount - resolvedDamage); LogDefenseResolution(damageContext, false, resolvedDamage); OnDefenseResolved?.Invoke(false, lastPreventedDamage); return resolvedDamage; } private void EnsureReferences() { if (sustainController == null) sustainController = GetComponent(); } private bool IsWithinGuardArc(GameObject sourceObject) { if (sourceObject == null) return true; Vector3 toSource = sourceObject.transform.position - transform.position; toSource.y = 0f; if (toSource.sqrMagnitude <= 0.0001f) return true; float dot = Vector3.Dot(transform.forward.normalized, toSource.normalized); float threshold = Mathf.Cos(guardHalfAngle * Mathf.Deg2Rad); return dot >= threshold; } private void ClearDefenseState() { isDefenseStateActive = false; defenseStateElapsed = 0f; lastPreventedDamage = 0f; lastWasPerfectGuard = false; perfectGuardAvailable = false; } private void LogDefenseResolution(DamageContext damageContext, bool isPerfectGuard, float resolvedDamage) { if (!enableDefenseDebugLogs) return; GameObject sourceObject = damageContext.SourceGameObject; string sourceName = sourceObject != null ? sourceObject.name : "Unknown"; string sourceSkillName = ResolveSourceSkillName(sourceObject); string guardType = isPerfectGuard ? "완벽 방어" : "방어"; Debug.Log( $"[Defense] {guardType} 성공 | owner={gameObject.name} | source={sourceName} | skill={sourceSkillName} | incoming={damageContext.Amount:F2} | prevented={lastPreventedDamage:F2} | resolved={resolvedDamage:F2} | elapsed={defenseStateElapsed:F3}s | mitigation={damageContext.MitigationTier}"); } private static string ResolveSourceSkillName(GameObject sourceObject) { if (sourceObject == null) return "None"; SkillController skillController = sourceObject.GetComponent(); if (skillController == null) skillController = sourceObject.GetComponentInParent(); if (skillController?.CurrentSkill == null) return "None"; return skillController.CurrentSkill.SkillName; } } }