feat: 방어 시스템과 드로그 검증 경로 정리

- 애니메이션 이벤트 기반 방어/유지/해제 흐름과 HUD 피드백, 방어 디버그 로그를 추가했다.
- 드로그 기본기1 테스트 패턴을 정리하고 공격 판정을 OnEffect 기반으로 옮기며 드로그 범위 효과의 타겟 레이어를 정상화했다.
- 플레이어 퀵슬롯 테스트 세팅과 적-플레이어 겹침 방지 로직을 조정해 충돌 시 적이 수평 이동을 멈추고 최소 분리만 수행하게 했다.
This commit is contained in:
2026-04-07 21:28:52 +09:00
parent 147e9baa25
commit 0c9967d131
72 changed files with 231096 additions and 698 deletions

View File

@@ -26,6 +26,9 @@ namespace Colosseum.Player
[Tooltip("이상상태 관리자 (없으면 자동 검색)")]
[SerializeField] private AbnormalityManager abnormalityManager;
[Tooltip("방어 상태 관리자 (없으면 자동 검색)")]
[SerializeField] private PlayerDefenseController defenseController;
[Header("Shield")]
[Tooltip("보호막 타입이 지정되지 않았을 때 사용할 기본 보호막 이상상태 데이터")]
[SerializeField] private AbnormalityData shieldStateAbnormality;
@@ -101,6 +104,13 @@ namespace Colosseum.Player
abnormalityManager = GetComponent<AbnormalityManager>();
}
if (defenseController == null)
{
defenseController = GetComponent<PlayerDefenseController>();
if (defenseController == null)
defenseController = gameObject.AddComponent<PlayerDefenseController>();
}
EnsurePassiveRuntimeReferences();
currentHealth.OnValueChanged += HandleHealthChanged;
@@ -197,7 +207,7 @@ namespace Colosseum.Player
[Rpc(SendTo.Server)]
public void TakeDamageRpc(float damage)
{
ApplyDamageInternal(damage, null);
ApplyDamageInternal(new DamageContext(damage));
}
/// <summary>
@@ -206,10 +216,7 @@ namespace Colosseum.Player
[Rpc(SendTo.Server)]
public void UseManaRpc(float amount)
{
if (isDead.Value)
return;
currentMana.Value = Mathf.Max(0f, currentMana.Value - amount);
SpendMana(amount);
}
/// <summary>
@@ -230,10 +237,7 @@ namespace Colosseum.Player
[Rpc(SendTo.Server)]
public void RestoreManaRpc(float amount)
{
if (isDead.Value)
return;
currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + amount);
RestoreMana(amount);
}
/// <summary>
@@ -665,7 +669,15 @@ namespace Colosseum.Player
/// </summary>
public float TakeDamage(float damage, object source = null)
{
return ApplyDamageInternal(damage, source);
return ApplyDamageInternal(new DamageContext(damage, source));
}
/// <summary>
/// 대미지 컨텍스트를 사용해 대미지를 적용합니다.
/// </summary>
public float TakeDamage(DamageContext damageContext)
{
return ApplyDamageInternal(damageContext);
}
/// <summary>
@@ -682,6 +694,32 @@ namespace Colosseum.Player
return actualHeal;
}
/// <summary>
/// 마나를 소모하고 실제 소모량을 반환합니다.
/// </summary>
public float SpendMana(float amount)
{
if (!IsServer || isDead.Value || amount <= 0f)
return 0f;
float actualSpent = Mathf.Min(amount, currentMana.Value);
currentMana.Value = Mathf.Max(0f, currentMana.Value - actualSpent);
return actualSpent;
}
/// <summary>
/// 마나를 회복하고 실제 회복량을 반환합니다.
/// </summary>
public float RestoreMana(float amount)
{
if (!IsServer || isDead.Value || amount <= 0f)
return 0f;
float actualRestore = Mathf.Min(amount, MaxMana - currentMana.Value);
currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + actualRestore);
return actualRestore;
}
/// <summary>
/// 보호막을 적용합니다.
/// </summary>
@@ -724,17 +762,29 @@ namespace Colosseum.Player
return remainingDamage;
}
private float ApplyDamageInternal(float damage, object source)
private float ApplyDamageInternal(DamageContext damageContext)
{
if (!IsServer || isDead.Value || IsDamageImmune())
return 0f;
float finalDamage = damage * GetIncomingDamageMultiplier();
if (defenseController == null)
defenseController = GetComponent<PlayerDefenseController>();
float rawDamage = damageContext.Amount;
if (rawDamage <= 0f)
return 0f;
if (defenseController != null)
{
rawDamage = defenseController.ResolveIncomingDamage(damageContext.WithAmount(rawDamage));
}
float finalDamage = rawDamage * GetIncomingDamageMultiplier();
float mitigatedDamage = ConsumeShield(finalDamage);
float actualDamage = Mathf.Min(mitigatedDamage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
CombatBalanceTracker.RecordDamage(source as GameObject, gameObject, actualDamage);
CombatBalanceTracker.RecordDamage(damageContext.SourceGameObject, gameObject, actualDamage);
if (currentHealth.Value <= 0f)
{