- 애니메이션 이벤트 기반 방어/유지/해제 흐름과 HUD 피드백, 방어 디버그 로그를 추가했다. - 드로그 기본기1 테스트 패턴을 정리하고 공격 판정을 OnEffect 기반으로 옮기며 드로그 범위 효과의 타겟 레이어를 정상화했다. - 플레이어 퀵슬롯 테스트 세팅과 적-플레이어 겹침 방지 로직을 조정해 충돌 시 적이 수평 이동을 멈추고 최소 분리만 수행하게 했다.
226 lines
7.9 KiB
C#
226 lines
7.9 KiB
C#
using System;
|
|
|
|
using UnityEngine;
|
|
|
|
using Colosseum.Combat;
|
|
using Colosseum.Skills;
|
|
|
|
namespace Colosseum.Player
|
|
{
|
|
/// <summary>
|
|
/// 플레이어의 방어 판정 상태를 관리합니다.
|
|
/// 마나 유지나 이동 감속 없이 순수하게 방어 가능 여부와 피해 감쇠만 처리합니다.
|
|
/// </summary>
|
|
[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;
|
|
|
|
/// <summary>
|
|
/// 방어 시작 시 완벽한 방어 유효 시간을 전달합니다.
|
|
/// </summary>
|
|
public event Action<float> OnDefenseStateEntered;
|
|
|
|
/// <summary>
|
|
/// 방어 성공 시 일반/완벽 여부와 막은 피해량을 전달합니다.
|
|
/// </summary>
|
|
public event Action<bool, float> OnDefenseResolved;
|
|
|
|
/// <summary>
|
|
/// 현재 방어 판정 활성 여부입니다.
|
|
/// </summary>
|
|
public bool IsDefenseStateActive => isDefenseStateActive;
|
|
|
|
/// <summary>
|
|
/// 방어 유지 시스템이 제공하는 이동 속도 배율입니다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 애니메이션 이벤트로 방어 판정을 시작합니다.
|
|
/// </summary>
|
|
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");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 애니메이션 이벤트로 방어 판정을 종료합니다.
|
|
/// </summary>
|
|
public void ExitDefenseState()
|
|
{
|
|
ClearDefenseState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 종료/취소 시 방어 판정을 정리합니다.
|
|
/// </summary>
|
|
public void HandleSkillExecutionEnded()
|
|
{
|
|
ClearDefenseState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 방어 판정이 유효하다면 피해를 감쇠합니다.
|
|
/// </summary>
|
|
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<PlayerDefenseSustainController>();
|
|
}
|
|
|
|
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<SkillController>();
|
|
if (skillController == null)
|
|
skillController = sourceObject.GetComponentInParent<SkillController>();
|
|
|
|
if (skillController?.CurrentSkill == null)
|
|
return "None";
|
|
|
|
return skillController.CurrentSkill.SkillName;
|
|
}
|
|
}
|
|
}
|