using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을 using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를 public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를 { [Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을 [SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을 [Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을 [SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을 [SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로 [Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을 [SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을 [SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를 [Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을 [SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 bossHealthBar를 // ⭐ [핵심 수정] 애니메이션 이름을 인스펙터에서 맞출 수 있게 변수로 분리 [Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")] [SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니메이션 이름을 [SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름을 [SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을 [SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 무기 줍기 애니메이션 이름을 [SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니메이션 이름을 [Header("--- 🎬 스킬 애니메이션 이름 ---")] [SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니메이션 이름을 [SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 출발 애니메이션 이름을 [SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 내려찍기 기모으기 이름을 [SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 내려찍기 타격 이름을 [SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니메이션 이름을 [Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🔍 디버그 ---를 [SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 보스 행동을 문장으로 보여줄 debugState를 // --- 내부 변수들 --- private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를 private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를 private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를 private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로 private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로 private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로 private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을 protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를 { base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을 rb = GetComponent(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에 if (ironBall != null) ballRb = ironBall.GetComponent(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에 } private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를 { StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을 } protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을 { base.Init(); // 부모 초기화 호출 _timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로 isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로 isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로 isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로 IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로 mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로 optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로 GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을 if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면 { target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에 playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를 } else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면 { var playerScript = FindObjectOfType(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를 if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면 { target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에 playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를 } } if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면 Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를 currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로 StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를 if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를 if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를 if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을 } private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를 { yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을 if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면 { int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로 while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안 { retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼 yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을 } if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로 agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로 } } protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를 { base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등) if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면 { ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을 } } // ════════════════════════════════════════ // 🧠 AI 핵심 로직 // ════════════════════════════════════════ protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을 { if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면 if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면 { RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을 return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을 } if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을 if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면 { _timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을 } float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를 if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면 if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면 { agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고 agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로 LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을 UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을 } else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면 { agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로 LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을 if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면 { AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를 if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면 { animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로 } } if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면 { _timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로 Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를 DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을 } } } // ════════════════════════════════════════ // 🎬 전투 입장 // ════════════════════════════════════════ public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을 { if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을 StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을 } private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을 { Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를 isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로 if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를 if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를 if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을 yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을 if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을 isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로 isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로 Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를 if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면 { agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로 int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로 while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안 { retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼 yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을 } if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면 { agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고 Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를 } else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면 { Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를 } } } // ════════════════════════════════════════ // 🚶 이동 애니메이션 & 타겟팅 // ════════════════════════════════════════ private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을 { if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면 AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를 if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면 { if (!state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 아직 걷기 모션이 아니라면 animator.Play(anim_Walk); // 애니메이션을 바꿀거에요 -> 걷기 모션으로 } else // 조건이 틀리면 실행할거에요 -> 거의 멈춰있다면 { if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) // 조건이 맞으면 실행할거에요 -> 대기나 포효 모션이 아니라면 animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로 } } private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을 { Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을 dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로 if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면 { transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게 transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을 Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을 Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로 ); } } // ════════════════════════════════════════ // 🧠 공격 판단 // ════════════════════════════════════════ private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을 { if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를 string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에 Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을 switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서 { case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을 case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을 case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을 default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을 } } // ════════════════════════════════════════ // 🏃 무기 회수 // ════════════════════════════════════════ private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을 { if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면 agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로 agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고 UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록 if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면 StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을 } private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을 { OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를 if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 줍기 위해 이동을 멈추고 속도를 0으로 if (animator != null) animator.Play(anim_Pickup); // 조건이 맞으면 실행할거에요 -> 줍는 애니메이션을 yield return new WaitForSeconds(0.8f); // 기다릴거에요 -> 손이 바닥에 닿을 0.8초를 ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로 ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로 ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로 if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로 yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를 isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로 OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를 if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를 } // ════════════════════════════════════════ // ✅ 공격 시작/종료 처리 (핵심) // ════════════════════════════════════════ public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를 { isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로 isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로 if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고 } public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를 { StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을 } private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을 { isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로 isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함) if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을 // 딜레이 시간 (여기서 patternInterval 만큼 멍때림) yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼 isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로 isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로 } // ════════════════════════════════════════ // ⚔️ 공격 패턴들 // ════════════════════════════════════════ // 패턴 1: 돌진 공격 private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을 { OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로 if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을 yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를 if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을 if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 애니메이션을 yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 돌진이 끝날 1초를 if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 조건이 맞으면 실행할거에요 -> 속도를 0으로 멈추고 물리를 끄기로 if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 전투 중이면 길찾기를 다시 켜기로 OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 } // 패턴 2: 내려찍기 private IEnumerator Pattern_SmashAttack() // 코루틴 함수를 선언할거에요 -> 내려찍기 패턴인 Pattern_SmashAttack을 { OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 if (animator != null) animator.Play(anim_SmashCharge, 0, 0f); // 조건이 맞으면 실행할거에요 -> 기를 모으는 모션을 yield return new WaitForSeconds(1.2f); // 기다릴거에요 -> 기를 모을 1.2초를 if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 조건이 맞으면 실행할거에요 -> 내려찍는 모션을 yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 자세를 가다듬을 1초를 OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 } // 패턴 3: 휩쓸기 private IEnumerator Pattern_ShieldWall() // 코루틴 함수를 선언할거에요 -> 휩쓸기 패턴인 Pattern_ShieldWall을 { OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 if (animator != null) animator.Play(anim_Sweep, 0, 0f); // 조건이 맞으면 실행할거에요 -> 휩쓰는 모션을 yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> 휘두르기가 끝날 2초를 OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 } // 패턴 4: 공 던지기 private IEnumerator Pattern_ThrowBall() // 코루틴 함수를 선언할거에요 -> 쇠공 던지기 패턴인 Pattern_ThrowBall을 { OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를 if (animator != null) animator.Play(anim_Throw, 0, 0f); // 조건이 맞으면 실행할거에요 -> 던지는 모션을 yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 공을 던지기 직전인 0.5초를 if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면 { ironBall.transform.SetParent(null); // 부모를 바꿀거에요 -> 쇠공을 손에서 완전히 분리로 ballRb.isKinematic = false; // 기능을 켤거에요 -> 쇠공의 물리 연산을 활성화로 Vector3 dir = (target.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을 ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 힘을 가할거에요 -> 앞으로 20, 위로 5의 포물선 힘을 ballRb.angularDrag = 5f; // 마찰을 줄거에요 -> 쇠공이 멀리 안 굴러가게 회전 마찰 5를 } isWeaponless = true; // 상태를 바꿀거에요 -> 던졌으므로 맨손 상태를 참으로 yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 던지고 자세를 가다듬을 1초를 OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를 } // ════════════════════════════════════════ // 💥 피격 / 사망 // ════════════════════════════════════════ protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 보스가 맞았을 때 호출되는 OnStartHit을 { // 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머) // 만약 끊기게 하려면 여기서 isPerformingAction = false; } protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 보스 체력이 0이 되면 호출되는 OnDie를 { isBattleStarted = false; // 상태를 바꿀거에요 -> 전투가 끝났으므로 전투 플래그를 거짓으로 isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 플래그를 거짓으로 if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를 GiveBossXP(); // 함수를 실행할거에요 -> 경험치를 주는 GiveBossXP를 base.OnDie(); // 부모 클래스의 사망 처리를 호출할거에요 -> 애니메이션, 시체 제거 등 } private void GiveBossXP() // 함수를 선언할거에요 -> 보스 처치 보상을 주는 GiveBossXP를 { Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 출력할거에요 -> 보스 처치 시도 메시지를 if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 경험치 시스템이 존재한다면 ObsessionSystem.instance.AddRunXP(100); // 함수를 실행할거에요 -> 경험치 100을 주는 AddRunXP를 else // 조건이 틀리면 실행할거에요 -> 시스템을 못 찾았다면 Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 시스템 누락 경고를 Debug.Log("[BossController] 보스 처치 처리 완료!"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를 } }