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); // 플레이어 분리용 (레이어 의존 없이 CharacterController로 식별) private readonly Collider[] overlapBuffer = new Collider[8]; // 이벤트 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; } protected virtual void Update() { if (!IsServer || IsDead) return; OnServerUpdate(); } /// /// 서버 Update 확장 포인트 (하위 클래스에서 override) /// protected virtual void OnServerUpdate() { } /// /// NavMeshAgent position sync 및 OnAnimatorMove 이후에 실행됩니다. /// 보스가 이미 플레이어 안으로 들어온 경우 stoppingDistance 바깥으로 밀어냅니다. /// Update()에서의 isStopped 조작은 NavMeshAgent에 의해 덮어써지지만, /// LateUpdate()는 그 이후이므로 확실하게 보정됩니다. /// private void LateUpdate() { if (!IsServer || IsDead || navMeshAgent == null) return; // stoppingDistance가 0이면 radius 기반 fallback 사용 float stopDist = navMeshAgent.stoppingDistance > 0f ? navMeshAgent.stoppingDistance : navMeshAgent.radius + 0.5f; int count = Physics.OverlapSphereNonAlloc(transform.position, stopDist, overlapBuffer); for (int i = 0; i < count; i++) { // 레이어 무관하게 CharacterController 유무로 플레이어 식별 if (!overlapBuffer[i].TryGetComponent(out _)) continue; Vector3 toPlayer = overlapBuffer[i].transform.position - transform.position; toPlayer.y = 0f; float dist = toPlayer.magnitude; if (dist >= stopDist) continue; // 보스가 실제로 이동 중일 때만 밀어냄. // isStopped는 수동 설정 시만 true가 되므로, velocity로 실제 이동 여부를 판단. if (navMeshAgent.velocity.sqrMagnitude > 0.01f) { Vector3 pushDir = dist > 0.001f ? -toPlayer.normalized : -transform.forward; navMeshAgent.Warp(transform.position + pushDir * (stopDist - dist)); navMeshAgent.isStopped = true; } } } /// /// 보스 스킬 루트모션이 플레이어 방향으로 진입하는 것을 차단합니다. /// private void OnAnimatorMove() { if (!IsServer || animator == null || navMeshAgent == null) return; Vector3 deltaPosition = animator.deltaPosition; float blockRadius = Mathf.Max(navMeshAgent.stoppingDistance, navMeshAgent.radius + 0.5f); int count = Physics.OverlapSphereNonAlloc(transform.position, blockRadius, overlapBuffer); for (int i = 0; i < count; i++) { if (!overlapBuffer[i].TryGetComponent(out _)) continue; Vector3 toPlayer = overlapBuffer[i].transform.position - transform.position; toPlayer.y = 0f; if (toPlayer.sqrMagnitude < 0.0001f) continue; Vector3 deltaXZ = new Vector3(deltaPosition.x, 0f, deltaPosition.z); if (Vector3.Dot(deltaXZ, toPlayer.normalized) > 0f) { deltaPosition.x = 0f; deltaPosition.z = 0f; } } navMeshAgent.Move(deltaPosition); if (animator.deltaRotation != Quaternion.identity) transform.rotation *= animator.deltaRotation; } 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; } /// /// 사망 애니메이션 재생 (모든 클라이언트에서 실행) /// [Rpc(SendTo.Everyone)] private void PlayDeathAnimationRpc() { if (animator != null) { // EnemyAnimationController 비활성화 (더 이상 애니메이션 제어하지 않음) var animController = GetComponent(); if (animController != null) { animController.enabled = false; } // 모든 트리거 리셋 animator.ResetTrigger("Attack"); animator.ResetTrigger("Skill"); animator.ResetTrigger("Hit"); animator.ResetTrigger("Jump"); animator.ResetTrigger("Land"); animator.ResetTrigger("Die"); // 즉시 Die 상태로 전환 (다른 애니메이션 중단) animator.Play("Die", 0, 0f); } } /// /// 사망 처리 (서버에서 실행) /// protected virtual void HandleDeath() { isDead.Value = true; // 실행 중인 스킬 즉시 취소 var skillController = GetComponent(); if (skillController != null) { skillController.CancelSkill(); } // 모든 클라이언트에서 사망 애니메이션 재생 PlayDeathAnimationRpc(); 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); } } }