using System; using UnityEngine; using Unity.Netcode; using Colosseum.Abnormalities; using Colosseum.Stats; using Colosseum.Combat; using Colosseum.Skills; namespace Colosseum.Player { /// /// 플레이어 네트워크 상태 관리 (HP, MP 등) /// public class PlayerNetworkController : NetworkBehaviour, IDamageable { [Header("References")] [Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")] [SerializeField] private CharacterStats characterStats; [Tooltip("이상상태 관리자 (없으면 자동 검색)")] [SerializeField] private AbnormalityManager abnormalityManager; // 네트워크 동기화 변수 private NetworkVariable currentHealth = new NetworkVariable(100f); private NetworkVariable currentMana = new NetworkVariable(50f); private NetworkVariable isDead = new NetworkVariable(false); public float Health => currentHealth.Value; public float Mana => currentMana.Value; public float MaxHealth => characterStats != null ? characterStats.MaxHealth : 100f; public float MaxMana => characterStats != null ? characterStats.MaxMana : 50f; public CharacterStats Stats => characterStats; // 체력/마나 변경 이벤트 public event Action OnHealthChanged; // (oldValue, newValue) public event Action OnManaChanged; // (oldValue, newValue) // 사망 이벤트 public event Action OnDeath; public event Action OnDeathStateChanged; // (isDead) public event Action OnRespawned; // IDamageable 구현 public float CurrentHealth => currentHealth.Value; public bool IsDead => isDead.Value; public override void OnNetworkSpawn() { // CharacterStats 참조 확인 if (characterStats == null) { characterStats = GetComponent(); } if (abnormalityManager == null) { abnormalityManager = GetComponent(); } // 네트워크 변수 변경 콜백 등록 currentHealth.OnValueChanged += HandleHealthChanged; currentMana.OnValueChanged += HandleManaChanged; isDead.OnValueChanged += HandleDeathStateChanged; // 초기화 if (IsServer) { currentHealth.Value = MaxHealth; currentMana.Value = MaxMana; isDead.Value = false; } } public override void OnNetworkDespawn() { // 콜백 해제 currentHealth.OnValueChanged -= HandleHealthChanged; currentMana.OnValueChanged -= HandleManaChanged; isDead.OnValueChanged -= HandleDeathStateChanged; } private void HandleHealthChanged(float oldValue, float newValue) { OnHealthChanged?.Invoke(oldValue, newValue); } private void HandleManaChanged(float oldValue, float newValue) { OnManaChanged?.Invoke(oldValue, newValue); } private void HandleDeathStateChanged(bool oldValue, bool newValue) { OnDeathStateChanged?.Invoke(newValue); } /// /// 대미지 적용 (서버에서만 실행) /// [Rpc(SendTo.Server)] public void TakeDamageRpc(float damage) { if (isDead.Value || IsDamageImmune()) return; float finalDamage = damage * GetIncomingDamageMultiplier(); float actualDamage = Mathf.Min(finalDamage, currentHealth.Value); currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage); if (currentHealth.Value <= 0f) { HandleDeath(); } } /// /// 마나 소모 (서버에서만 실행) /// [Rpc(SendTo.Server)] public void UseManaRpc(float amount) { if (isDead.Value) return; currentMana.Value = Mathf.Max(0f, currentMana.Value - amount); } /// /// 체력 회복 (서버에서만 실행) /// [Rpc(SendTo.Server)] public void RestoreHealthRpc(float amount) { if (isDead.Value) return; currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount); } /// /// 마나 회복 (서버에서만 실행) /// [Rpc(SendTo.Server)] public void RestoreManaRpc(float amount) { if (isDead.Value) return; currentMana.Value = Mathf.Min(MaxMana, currentMana.Value + amount); } /// /// 사망 애니메이션 재생 (모든 클라이언트에서 실행) /// [Rpc(SendTo.Everyone)] private void PlayDeathAnimationRpc() { var animator = GetComponentInChildren(); if (animator != null) { animator.SetTrigger("Die"); } } /// /// 사망 처리 (서버에서만 실행) /// private void HandleDeath() { if (isDead.Value) return; isDead.Value = true; // 사망 시 활성 이상 상태를 정리해 리스폰 시 잔존하지 않게 합니다. if (abnormalityManager != null) { abnormalityManager.RemoveAllAbnormalities(); } // 이동 비활성화 var movement = GetComponent(); if (movement != null) { movement.ClearForcedMovement(); movement.enabled = false; } var hitReactionController = GetComponent(); if (hitReactionController != null) { hitReactionController.ClearHitReactionState(); } // 스킬 입력 비활성화 var skillInput = GetComponent(); if (skillInput != null) { skillInput.enabled = false; } // 실행 중인 스킬 즉시 취소 var skillController = GetComponent(); if (skillController != null) { skillController.CancelSkill(SkillCancelReason.Death); } // 모든 클라이언트에서 사망 애니메이션 재생 PlayDeathAnimationRpc(); // 사망 이벤트 발생 OnDeath?.Invoke(this); Debug.Log($"[Player] Player {OwnerClientId} died!"); } /// /// 리스폰 (서버에서만 실행) /// public void Respawn() { if (!IsServer) return; if (abnormalityManager != null) { abnormalityManager.RemoveAllAbnormalities(); } isDead.Value = false; currentHealth.Value = MaxHealth; currentMana.Value = MaxMana; // 이동 재활성화 var movement = GetComponent(); if (movement != null) { movement.ClearForcedMovement(); movement.enabled = true; } var hitReactionController = GetComponent(); if (hitReactionController != null) { hitReactionController.ClearHitReactionState(); } // 스킬 입력 재활성화 var skillInput = GetComponent(); if (skillInput != null) { skillInput.enabled = true; } // 애니메이션 리셋 var animator = GetComponentInChildren(); if (animator != null) { animator.Rebind(); } var skillController = GetComponent(); if (skillController != null) { skillController.CancelSkill(SkillCancelReason.Respawn); } OnRespawned?.Invoke(this); Debug.Log($"[Player] Player {OwnerClientId} respawned!"); } #region IDamageable /// /// 대미지 적용 (서버에서만 호출) /// public float TakeDamage(float damage, object source = null) { if (!IsServer || isDead.Value || IsDamageImmune()) return 0f; float finalDamage = damage * GetIncomingDamageMultiplier(); float actualDamage = Mathf.Min(finalDamage, currentHealth.Value); currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage); if (currentHealth.Value <= 0f) { HandleDeath(); } return actualDamage; } /// /// 체력 회복 (서버에서만 호출) /// public float Heal(float amount) { if (!IsServer || isDead.Value) return 0f; float actualHeal = Mathf.Min(amount, MaxHealth - currentHealth.Value); currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount); return actualHeal; } private bool IsDamageImmune() { return abnormalityManager != null && abnormalityManager.IsInvincible; } private float GetIncomingDamageMultiplier() { if (abnormalityManager == null) return 1f; return Mathf.Max(0f, abnormalityManager.IncomingDamageMultiplier); } #endregion } }