using UnityEngine; using UnityEngine.AI; using System.Collections; using System; /// /// 몬스터 기본 클래스 (공통 기능만) /// - 공격 방식은 자식 클래스에서 구현 /// public abstract class MonsterClass : MonoBehaviour, IDamageable { [Header("--- 최적화 설정 ---")] protected Renderer mobRenderer; protected Transform playerTransform; [SerializeField] protected float optimizationDistance = 40f; [Header("몬스터 기본 스탯")] [SerializeField] protected float maxHP = 100f; [SerializeField] protected float attackDamage = 10f; [SerializeField] protected int expReward = 10; [SerializeField] protected float moveSpeed = 3.5f; protected float currentHP; public event Action OnHealthChanged; [Header("전투 / 무기 (선택사항)")] // ⭐ 근접 몬스터가 사용할 무기 (원거리 몬스터는 비워둬도 됨) [SerializeField] protected MonsterWeapon myWeapon; [Header("피격 / 사망 / 대기 애니메이션")] [SerializeField] protected string Monster_Idle = "Monster_Idle"; [SerializeField] protected string Monster_GetDamage = "Monster_GetDamage"; [SerializeField] protected string Monster_Die = "Monster_Die"; protected Animator animator; protected NavMeshAgent agent; protected AudioSource audioSource; protected bool isHit, isDead, isAttacking; public bool IsAggroed { get; protected set; } [Header("AI 설정")] [SerializeField] protected float attackRestDuration = 1.5f; protected bool isResting; public static System.Action OnMonsterKilled; [Header("공통 사운드/이펙트")] [SerializeField] protected AudioClip hitSound, deathSound; [SerializeField] protected GameObject deathEffectPrefab; [SerializeField] protected ParticleSystem hitEffect; [SerializeField] protected Transform impactSpawnPoint; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 생명주기 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ protected virtual void Awake() { mobRenderer = GetComponentInChildren(); animator = GetComponent(); agent = GetComponent(); audioSource = GetComponent(); if (agent != null) agent.speed = moveSpeed; } protected virtual void OnEnable() { playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; if (mobRenderer != null) mobRenderer.enabled = true; Init(); if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.RegisterMob(this); } protected virtual void OnDisable() { if (MobUpdateManager.Instance != null) MobUpdateManager.Instance.UnregisterMob(this); } public void ResetStats() { isDead = false; IsAggroed = false; currentHP = maxHP; OnHealthChanged?.Invoke(currentHP, maxHP); Collider col = GetComponent(); if (col != null) col.enabled = true; if (agent != null) agent.speed = moveSpeed; // 스피드 초기화 OnResetStats(); } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 추상 메서드 (자식이 반드시 구현) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ protected virtual void Init() { } protected abstract void ExecuteAILogic(); protected virtual void OnResetStats() { } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 공통 기능 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ public virtual void Reactivate() { if (agent != null && agent.isOnNavMesh) { agent.isStopped = false; if (IsAggroed && playerTransform != null) { agent.SetDestination(playerTransform.position); } } if (mobRenderer != null) mobRenderer.enabled = true; } public void OnManagedUpdate() { if (isDead || playerTransform == null || !gameObject.activeInHierarchy) return; float distance = Vector3.Distance(transform.position, playerTransform.position); if (distance > optimizationDistance && !IsAggroed) { StopMovement(); if (mobRenderer != null && mobRenderer.enabled) mobRenderer.enabled = false; return; } if (mobRenderer != null && !mobRenderer.enabled) mobRenderer.enabled = true; if (mobRenderer != null && !mobRenderer.isVisible) { StopMovement(); return; } if (agent != null && agent.isOnNavMesh && agent.isStopped) agent.isStopped = false; ExecuteAILogic(); } protected void StopMovement() { if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } if (animator != null) { animator.SetFloat("Speed", 0f); animator.Play(Monster_Idle); } } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 피격 / 사망 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ public virtual void TakeDamage(float amount) { OnDamaged(amount); } public virtual void OnDamaged(float damage) { if (isDead) return; IsAggroed = true; currentHP -= damage; OnHealthChanged?.Invoke(currentHP, maxHP); if (currentHP <= 0) { Die(); return; } if (!isHit) StartHit(); } protected virtual void StartHit() { isHit = true; isAttacking = false; isResting = false; StopAllCoroutines(); OnStartHit(); if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } animator.Play(Monster_GetDamage, 0, 0f); if (hitEffect) hitEffect.Play(); if (hitSound) audioSource.PlayOneShot(hitSound); } protected virtual void OnStartHit() { } public virtual void OnHitEnd() { isHit = false; if (agent && agent.isOnNavMesh) agent.isStopped = false; } protected virtual void Die() { if (isDead) return; isDead = true; IsAggroed = false; OnDie(); OnMonsterKilled?.Invoke(expReward); Collider col = GetComponent(); if (col != null) col.enabled = false; if (agent && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } animator.Play(Monster_Die, 0, 0f); if (deathSound) audioSource.PlayOneShot(deathSound); Invoke("ReturnToPool", 1.5f); } protected virtual void OnDie() { } public bool IsDead => isDead; protected void ReturnToPool() => gameObject.SetActive(false); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ⭐ 공격 이벤트 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ public virtual void OnAttackStart() { isAttacking = true; isResting = false; if (myWeapon != null) { myWeapon.EnableHitBox(); } } public virtual void OnAttackEnd() { if (myWeapon != null) { myWeapon.DisableHitBox(); } isAttacking = false; if (!isDead && !isHit) StartCoroutine(RestAfterAttack()); } protected virtual IEnumerator RestAfterAttack() { isResting = true; yield return new WaitForSeconds(attackRestDuration); isResting = false; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // ⭐ 상태 이상 시스템 (추가된 부분) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /// /// 상태이상 적용 (PlayerArrow에서 호출) /// public void ApplyStatusEffect(StatusEffectType type, float damage, float duration) { if (isDead) return; switch (type) { case StatusEffectType.Burn: StartCoroutine(BurnCoroutine(damage, duration)); break; case StatusEffectType.Slow: StartCoroutine(SlowCoroutine(damage, duration)); break; case StatusEffectType.Poison: StartCoroutine(PoisonCoroutine(damage, duration)); break; case StatusEffectType.Shock: TakeDamage(damage); // 충격 데미지 즉시 적용 StartCoroutine(StunCoroutine(0.5f)); // 0.5초 스턴 break; } } /// /// 화상: 일정 시간 동안 0.5초마다 틱 데미지 /// private IEnumerator BurnCoroutine(float tickDamage, float duration) { float elapsed = 0f; float tickInterval = 0.5f; while (elapsed < duration) { TakeDamage(tickDamage * tickInterval); yield return new WaitForSeconds(tickInterval); elapsed += tickInterval; } } /// /// 슬로우: 이동속도를 일시적으로 감소 /// private IEnumerator SlowCoroutine(float amount, float duration) { // NavMeshAgent가 있는 경우 속도 조절 if (agent != null) { float originalSpeed = agent.speed; agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); yield return new WaitForSeconds(duration); agent.speed = originalSpeed; // 원래 속도로 복구 } else { // NavMeshAgent가 없으면 그냥 대기 yield return new WaitForSeconds(duration); } } /// /// 독: 지속 데미지 (1초마다) /// private IEnumerator PoisonCoroutine(float tickDamage, float duration) { float elapsed = 0f; float tickInterval = 1f; while (elapsed < duration) { TakeDamage(tickDamage * tickInterval); yield return new WaitForSeconds(tickInterval); elapsed += tickInterval; } } /// /// 스턴: 짧은 시간 동안 행동 정지 /// private IEnumerator StunCoroutine(float duration) { if (agent != null) { agent.isStopped = true; if (animator != null) animator.speed = 0; // 애니메이션도 멈춤 (선택사항) yield return new WaitForSeconds(duration); if (!isDead) { agent.isStopped = false; if (animator != null) animator.speed = 1; } } else { yield return new WaitForSeconds(duration); } } }