using System; using UnityEngine; using Unity.Netcode; using Colosseum.Stats; using Colosseum.Combat; namespace Colosseum.Enemy { /// /// 적 캐릭터 기본 클래스. /// 네트워크 동기화, 스탯 관리, 대미지 처리를 담당합니다. /// public class EnemyBase : NetworkBehaviour, IDamageable { [Header("References")] [Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")] [SerializeField] protected CharacterStats characterStats; [Tooltip("Animator 컴포넌트")] [SerializeField] protected Animator animator; [Tooltip("NavMeshAgent 또는 이동 컴포넌트")] [SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent; [Header("Data")] [SerializeField] protected EnemyData enemyData; // 네트워크 동기화 변수 protected NetworkVariable currentHealth = new NetworkVariable(100f); protected NetworkVariable currentMana = new NetworkVariable(50f); protected NetworkVariable isDead = new NetworkVariable(false); // 이벤트 public event Action OnHealthChanged; // currentHealth, maxHealth public event Action OnDamageTaken; // damage public event Action OnDeath; // Properties public float CurrentHealth => currentHealth.Value; public float MaxHealth => characterStats != null ? characterStats.MaxHealth : 100f; public float CurrentMana => currentMana.Value; public float MaxMana => characterStats != null ? characterStats.MaxMana : 50f; public bool IsDead => isDead.Value; public CharacterStats Stats => characterStats; public EnemyData Data => enemyData; public Animator Animator => animator; public override void OnNetworkSpawn() { // 컴포넌트 참조 확인 if (characterStats == null) characterStats = GetComponent(); if (animator == null) animator = GetComponentInChildren(); if (navMeshAgent == null) navMeshAgent = GetComponent(); // 서버에서 초기화 if (IsServer) { InitializeStats(); } // 클라이언트에서 체력 변화 감지 currentHealth.OnValueChanged += OnHealthChangedInternal; } public override void OnNetworkDespawn() { currentHealth.OnValueChanged -= OnHealthChangedInternal; } /// /// 스탯 초기화 (서버에서만 실행) /// protected virtual void InitializeStats() { if (enemyData != null && characterStats != null) { enemyData.ApplyBaseStats(characterStats); } // NavMeshAgent 속도 설정 if (navMeshAgent != null && enemyData != null) { navMeshAgent.speed = enemyData.MoveSpeed; navMeshAgent.angularSpeed = enemyData.RotationSpeed; } currentHealth.Value = MaxHealth; currentMana.Value = MaxMana; isDead.Value = false; } /// /// 대미지 적용 (서버에서 실행) /// public virtual float TakeDamage(float damage, object source = null) { if (!IsServer || isDead.Value) return 0f; float actualDamage = Mathf.Min(damage, currentHealth.Value); currentHealth.Value = Mathf.Max(0f, currentHealth.Value - actualDamage); OnDamageTaken?.Invoke(actualDamage); // 대미지 피드백 (애니메이션, 이펙트 등) OnTakeDamageFeedback(actualDamage, source); if (currentHealth.Value <= 0f) { HandleDeath(); } return actualDamage; } /// /// 대미지 피드백 (애니메이션, 이펙트) /// protected virtual void OnTakeDamageFeedback(float damage, object source) { if (animator != null) { animator.SetTrigger("Hit"); } } /// /// 체력 회복 (서버에서 실행) /// public virtual float Heal(float amount) { if (!IsServer || isDead.Value) return 0f; float oldHealth = currentHealth.Value; currentHealth.Value = Mathf.Min(MaxHealth, currentHealth.Value + amount); float actualHeal = currentHealth.Value - oldHealth; return actualHeal; } /// /// 사망 처리 (서버에서 실행) /// protected virtual void HandleDeath() { isDead.Value = true; if (animator != null) { animator.SetTrigger("Die"); } if (navMeshAgent != null) { navMeshAgent.isStopped = true; } OnDeath?.Invoke(); Debug.Log($"[Enemy] {name} died!"); } /// /// 리스폰 /// public virtual void Respawn() { if (!IsServer) return; isDead.Value = false; InitializeStats(); if (navMeshAgent != null) { navMeshAgent.isStopped = false; } if (animator != null) { animator.Rebind(); } } // 체력 변화 이벤트 전파 private void OnHealthChangedInternal(float oldValue, float newValue) { OnHealthChanged?.Invoke(newValue, MaxHealth); } } }