feat: 플레이어 탱킹 및 지원 스킬 1차 구현

- 도발, 방어 태세, 철벽 스킬과 위협 생성 배율 시스템을 추가

- 치유, 광역 치유, 보호막 스킬과 관련 이상상태/이펙트 자산을 구성

- 보호막 흡수 로직과 체력 HUD 보너스 표시를 PlayerNetworkController, PlayerHUD, StatBar에 반영

- 플레이어 프리팹 슬롯과 디버그 메뉴를 확장해 탱킹·지원 스킬 검증 경로를 추가

- Unity 컴파일과 런타임 테스트에서 도발, 치유, 광역 치유, 보호막 발동 및 보호막 수치 적용을 확인
This commit is contained in:
2026-03-24 19:17:16 +09:00
parent c4209855ab
commit 0c7c7b0c12
48 changed files with 1200 additions and 13 deletions

View File

@@ -25,9 +25,11 @@ namespace Colosseum.Player
private NetworkVariable<float> currentHealth = new NetworkVariable<float>(100f);
private NetworkVariable<float> currentMana = new NetworkVariable<float>(50f);
private NetworkVariable<bool> isDead = new NetworkVariable<bool>(false);
private NetworkVariable<float> currentShield = new NetworkVariable<float>(0f);
public float Health => currentHealth.Value;
public float Mana => currentMana.Value;
public float Shield => currentShield.Value;
public float MaxHealth => characterStats != null ? characterStats.MaxHealth : 100f;
public float MaxMana => characterStats != null ? characterStats.MaxMana : 50f;
public CharacterStats Stats => characterStats;
@@ -35,6 +37,7 @@ namespace Colosseum.Player
// 체력/마나 변경 이벤트
public event Action<float, float> OnHealthChanged; // (oldValue, newValue)
public event Action<float, float> OnManaChanged; // (oldValue, newValue)
public event Action<float, float> OnShieldChanged; // (oldValue, newValue)
// 사망 이벤트
public event Action<PlayerNetworkController> OnDeath;
@@ -61,6 +64,7 @@ namespace Colosseum.Player
// 네트워크 변수 변경 콜백 등록
currentHealth.OnValueChanged += HandleHealthChanged;
currentMana.OnValueChanged += HandleManaChanged;
currentShield.OnValueChanged += HandleShieldChanged;
isDead.OnValueChanged += HandleDeathStateChanged;
// 초기화
@@ -68,6 +72,7 @@ namespace Colosseum.Player
{
currentHealth.Value = MaxHealth;
currentMana.Value = MaxMana;
currentShield.Value = 0f;
isDead.Value = false;
}
}
@@ -77,6 +82,7 @@ namespace Colosseum.Player
// 콜백 해제
currentHealth.OnValueChanged -= HandleHealthChanged;
currentMana.OnValueChanged -= HandleManaChanged;
currentShield.OnValueChanged -= HandleShieldChanged;
isDead.OnValueChanged -= HandleDeathStateChanged;
}
@@ -90,6 +96,11 @@ namespace Colosseum.Player
OnManaChanged?.Invoke(oldValue, newValue);
}
private void HandleShieldChanged(float oldValue, float newValue)
{
OnShieldChanged?.Invoke(oldValue, newValue);
}
private void HandleDeathStateChanged(bool oldValue, bool newValue)
{
OnDeathStateChanged?.Invoke(newValue);
@@ -104,7 +115,8 @@ namespace Colosseum.Player
if (isDead.Value || IsDamageImmune()) return;
float finalDamage = damage * GetIncomingDamageMultiplier();
float actualDamage = Mathf.Min(finalDamage, currentHealth.Value);
float mitigatedDamage = ConsumeShield(finalDamage);
float actualDamage = Mathf.Min(mitigatedDamage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
if (currentHealth.Value <= 0f)
@@ -167,6 +179,7 @@ namespace Colosseum.Player
if (isDead.Value) return;
isDead.Value = true;
currentShield.Value = 0f;
// 사망 시 활성 이상 상태를 정리해 리스폰 시 잔존하지 않게 합니다.
if (abnormalityManager != null)
@@ -188,6 +201,12 @@ namespace Colosseum.Player
hitReactionController.ClearHitReactionState();
}
var threatController = GetComponent<ThreatController>();
if (threatController != null)
{
threatController.ClearThreatModifiers();
}
// 스킬 입력 비활성화
var skillInput = GetComponent<PlayerSkillInput>();
if (skillInput != null)
@@ -226,6 +245,7 @@ namespace Colosseum.Player
isDead.Value = false;
currentHealth.Value = MaxHealth;
currentMana.Value = MaxMana;
currentShield.Value = 0f;
// 이동 재활성화
var movement = GetComponent<PlayerMovement>();
@@ -241,6 +261,12 @@ namespace Colosseum.Player
hitReactionController.ClearHitReactionState();
}
var threatController = GetComponent<ThreatController>();
if (threatController != null)
{
threatController.ClearThreatModifiers();
}
// 스킬 입력 재활성화
var skillInput = GetComponent<PlayerSkillInput>();
if (skillInput != null)
@@ -275,7 +301,8 @@ namespace Colosseum.Player
if (!IsServer || isDead.Value || IsDamageImmune()) return 0f;
float finalDamage = damage * GetIncomingDamageMultiplier();
float actualDamage = Mathf.Min(finalDamage, currentHealth.Value);
float mitigatedDamage = ConsumeShield(finalDamage);
float actualDamage = Mathf.Min(mitigatedDamage, currentHealth.Value);
currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage);
if (currentHealth.Value <= 0f)
@@ -299,6 +326,23 @@ namespace Colosseum.Player
return actualHeal;
}
/// <summary>
/// 보호막을 적용합니다.
/// </summary>
public void ApplyShield(float amount, float duration)
{
if (!IsServer || isDead.Value || amount <= 0f)
return;
currentShield.Value = Mathf.Max(currentShield.Value, amount);
if (duration > 0f)
{
CancelInvoke(nameof(ClearShield));
Invoke(nameof(ClearShield), duration);
}
}
private bool IsDamageImmune()
{
return abnormalityManager != null && abnormalityManager.IsInvincible;
@@ -311,6 +355,24 @@ namespace Colosseum.Player
return Mathf.Max(0f, abnormalityManager.IncomingDamageMultiplier);
}
private float ConsumeShield(float incomingDamage)
{
if (incomingDamage <= 0f || currentShield.Value <= 0f)
return incomingDamage;
float shieldAbsorb = Mathf.Min(currentShield.Value, incomingDamage);
currentShield.Value = Mathf.Max(0f, currentShield.Value - shieldAbsorb);
return Mathf.Max(0f, incomingDamage - shieldAbsorb);
}
private void ClearShield()
{
if (!IsServer)
return;
currentShield.Value = 0f;
}
#endregion
}
}