Projext/Assets/Scripts/Enemy/BossAI/BossMonster.cs

560 lines
40 KiB
C#

using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
using System.Collections.Generic; // Dictionary를 사용할거에요 -> System.Collections.Generic을
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
{ // 코드 블록을 시작할거에요 -> NorcielBoss 범위를
[Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 제목을 달거에요 -> 두뇌 연결 설정을
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 패턴 결정을 위한 카운터 시스템을
[Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 제목을 달거에요 -> 패턴 설정을
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 딜레이 시간을
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 공격이 가능한 사거리를
[Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 제목을 달거에요 -> 무기 설정을
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 보스가 던질 쇠공 오브젝트를
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 쇠공을 쥘 손의 위치를
[Header("--- 📊 UI 연결 ---")] // 인스펙터 제목을 달거에요 -> UI 설정을
[SerializeField] private GameObject 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 = "대기 중"; // 변수를 선언할거에요 -> 현재 상태를 보여줄 텍스트를
private float _timer; // 변수를 선언할거에요 -> 패턴 쿨타임용 타이머를
private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 몸체를
private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 몸체를
private bool isBattleStarted = false; // 변수를 설정할거에요 -> 전투 시작 여부를 (기본 거짓)
private bool isWeaponless = false; // 변수를 설정할거에요 -> 무기 분실 여부를 (기본 거짓)
private bool isPerformingAction = false; // 변수를 설정할거에요 -> 행동 수행 중 여부를 (기본 거짓)
private Transform target; // 변수를 선언할거에요 -> 공격 대상(플레이어)을
private readonly Dictionary<string, float> _clipLengthCache = new Dictionary<string, float>(); // 딕셔너리를 선언할거에요 -> 애니메이션 길이를 저장할 캐시를
protected override void Awake() // 함수를 실행할거에요 -> 객체 생성 시 초기화를
{ // 코드 블록을 시작할거에요 -> Awake 범위를
base.Awake(); // 부모의 Awake를 실행할거에요
rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 내 몸의 Rigidbody를
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); // 조건이 맞으면 가져올거에요 -> 쇠공의 Rigidbody를
} // 코드 블록을 끝낼거에요 -> Awake를
private void Start() // 함수를 실행할거에요 -> 게임 시작 시 초기화를
{ // 코드 블록을 시작할거에요 -> Start 범위를
CacheAllClipLengths(); // 함수를 실행할거에요 -> 애니메이션 길이를 미리 저장하는 기능을
StartBossBattle(); // 함수를 실행할거에요 -> 보스전을 시작하는 기능을
} // 코드 블록을 끝낼거에요 -> Start를
private void CacheAllClipLengths() // 함수를 정의할거에요 -> 애니메이션 길이를 캐싱하는 로직을
{ // 코드 블록을 시작할거에요 -> CacheAllClipLengths 범위를
_clipLengthCache.Clear(); // 비울거에요 -> 기존 캐시 데이터를
if (animator == null || animator.runtimeAnimatorController == null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 없다면
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
Debug.LogError("❌ [Boss] Animator 또는 RuntimeAnimatorController가 없습니다! 클립 길이를 캐싱할 수 없습니다."); // 에러를 출력할거에요
return; // 중단할거에요
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
AnimationClip[] clips = animator.runtimeAnimatorController.animationClips; // 배열을 가져올거에요 -> 모든 애니메이션 클립을
foreach (AnimationClip clip in clips) // 반복할거에요 -> 모든 클립을 돌면서
{ // 코드 블록을 시작할거에요 -> foreach 범위를
if (!_clipLengthCache.ContainsKey(clip.name)) // 조건이 맞으면 실행할거에요 -> 이름이 캐시에 없다면
{ // 코드 블록을 시작할거에요 -> 캐시 저장 범위를
_clipLengthCache[clip.name] = clip.length; // 저장할거에요 -> 원본 이름을 키로 길이를
} // 코드 블록을 끝낼거에요 -> 캐시 저장 끝
int pipeIndex = clip.name.IndexOf('|'); // 위치를 찾을거에요 -> "|" 기호의 위치를
if (pipeIndex >= 0 && pipeIndex < clip.name.Length - 1) // 조건이 맞으면 실행할거에요 -> "|" 기호가 존재하면
{ // 코드 블록을 시작할거에요 -> 단축 이름 처리 범위를
string shortName = clip.name.Substring(pipeIndex + 1); // 추출할거에요 -> "|" 뒤의 짧은 이름을
if (!_clipLengthCache.ContainsKey(shortName)) // 조건이 맞으면 실행할거에요 -> 짧은 이름이 캐시에 없다면
{ // 코드 블록을 시작할거에요 -> 단축 이름 캐시 저장 범위를
_clipLengthCache[shortName] = clip.length; // 저장할거에요 -> 짧은 이름도 키로 등록
} // 코드 블록을 끝낼거에요 -> 단축 이름 저장 끝
} // 코드 블록을 끝낼거에요 -> 단축 이름 처리 끝
Debug.Log($"🎬 [Boss] 클립 캐싱: \"{clip.name}\" = {clip.length:F2}초"); // 로그를 찍을거에요 -> 캐싱 정보를
} // 코드 블록을 끝낼거에요 -> foreach 끝
Debug.Log($"✅ [Boss] 총 {_clipLengthCache.Count}개 키 캐싱 완료! (원본 + 단축이름 포함)"); // 로그를 찍을거에요 -> 완료 정보를
} // 코드 블록을 끝낼거에요 -> CacheAllClipLengths를
private float GetClipLength(string clipName, float fallback = 1.0f) // 함수를 정의할거에요 -> 이름으로 길이를 찾는 기능을
{ // 코드 블록을 시작할거에요 -> GetClipLength 범위를
if (_clipLengthCache.TryGetValue(clipName, out float length)) // 조건이 맞으면 실행할거에요 -> 캐시에서 바로 찾았다면
{ // 코드 블록을 시작할거에요 -> 즉시 반환 범위를
return length; // 반환할거에요 -> 찾은 길이를
} // 코드 블록을 끝낼거에요 -> 즉시 반환 끝
foreach (KeyValuePair<string, float> kvp in _clipLengthCache) // 반복할거에요 -> 캐시 전체를 뒤져서
{ // 코드 블록을 시작할거에요 -> 부분 매칭 범위를
if (kvp.Key.EndsWith(clipName)) // 조건이 맞으면 실행할거에요 -> 이름 끝부분이 일치한다면
{ // 코드 블록을 시작할거에요 -> 매칭 성공 범위를
_clipLengthCache[clipName] = kvp.Value; // 저장할거에요 -> 다음 검색을 위해 짧은 이름도 캐싱
Debug.Log($"🔗 [Boss] 부분 매칭 성공: \"{clipName}\" → \"{kvp.Key}\" ({kvp.Value:F2}초)"); // 로그를 찍을거에요
return kvp.Value; // 반환할거에요 -> 매칭된 길이를
} // 코드 블록을 끝낼거에요 -> 매칭 성공 끝
} // 코드 블록을 끝낼거에요 -> 부분 매칭 끝
Debug.LogWarning($"⚠️ [Boss] 클립 \"{clipName}\"을 찾지 못했습니다! fallback={fallback:F2}초 사용"); // 경고를 찍을거에요 -> 못 찾았을 때
return fallback; // 반환할거에요 -> 기본값을
} // 코드 블록을 끝낼거에요 -> GetClipLength를
private IEnumerator PlayAndWait(string animName, float fallback = 1.0f) // 코루틴을 정의할거에요 -> 애니 재생 후 길이만큼 대기하는 기능을
{ // 코드 블록을 시작할거에요 -> PlayAndWait 범위를
if (animator == null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 없다면
{ // 코드 블록을 시작할거에요 -> 예외 처리 범위를
Debug.LogWarning($"⚠️ [Boss] Animator 없음! \"{animName}\" fallback={fallback}초 대기"); // 경고를 찍을거에요
yield return new WaitForSeconds(Mathf.Max(0.1f, fallback)); // 대기할거에요 -> 최소 0.1초 또는 기본값만큼
yield break; // 종료할거에요
} // 코드 블록을 끝낼거에요 -> 예외 처리 끝
int stateHash = Animator.StringToHash(animName); // 계산할거에요 -> 상태 이름의 해시값을
bool stateExists = animator.HasState(0, stateHash); // 확인할거에요 -> 상태가 실제로 존재하는지
if (!stateExists) // 조건이 맞으면 실행할거에요 -> 상태가 존재하지 않는다면
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
Debug.LogError($"❌ [Boss] Animator에 State \"{animName}\"이 없습니다! Animator Controller를 확인하세요."); // 에러를 찍을거에요
yield return new WaitForSeconds(Mathf.Max(0.1f, fallback)); // 대기할거에요 -> 기본값만큼
yield break; // 종료할거에요
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
animator.Play(animName, 0, 0f); // 재생할거에요 -> 해당 애니메이션을 처음부터
yield return null; // 대기할거에요 -> 1프레임 동안 (상태 갱신)
float duration = GetClipLength(animName, -1f); // 가져올거에요 -> 캐싱된 길이를
if (duration < 0f) // 조건이 맞으면 실행할거에요 -> 캐시에 없는 경우
{ // 코드 블록을 시작할거에요 -> State 길이로 보정 범위를
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 상태 정보를
duration = stateInfo.length; // 가져올거에요 -> 상태의 길이를
if (duration > 0f) // 조건이 맞으면 실행할거에요 -> 유효한 길이면
{ // 코드 블록을 시작할거에요 -> 캐싱 범위를
_clipLengthCache[animName] = duration; // 저장할거에요 -> 캐시에 등록
Debug.Log($"🎬 [Boss] State에서 길이 확인 & 캐싱: \"{animName}\" = {duration:F2}초"); // 로그를 찍을거에요
} // 코드 블록을 끝낼거에요 -> 캐싱 끝
else // 조건이 틀리면 실행할거에요 -> 길이가 이상하면
{ // 코드 블록을 시작할거에요 -> fallback 적용 범위를
duration = Mathf.Max(0.1f, fallback); // 설정할거에요 -> 기본값으로 보장
Debug.LogWarning($"⚠️ [Boss] State \"{animName}\" 길이 비정상({stateInfo.length}초)! fallback={duration}초 사용"); // 경고를 찍을거에요
} // 코드 블록을 끝낼거에요 -> fallback 적용 끝
} // 코드 블록을 끝낼거에요 -> State 길이 보정 끝
float remaining = duration - Time.deltaTime; // 계산할거에요 -> 1프레임을 뺀 남은 시간을
if (remaining > 0f) // 조건이 맞으면 실행할거에요 -> 남은 시간이 양수면
{ // 코드 블록을 시작할거에요 -> 대기 범위를
yield return new WaitForSeconds(remaining); // 기다릴거에요 -> 남은 시간만큼
} // 코드 블록을 끝낼거에요 -> 대기 끝
} // 코드 블록을 끝낼거에요 -> PlayAndWait를
protected override void Init() // 함수를 정의할거에요 -> 초기화 설정을
{ // 코드 블록을 시작할거에요 -> Init 범위를
base.Init(); // 부모의 초기화를 호출할거에요
_timer = 0f; // 초기화할거에요 -> 타이머를 0으로
isBattleStarted = false; // 설정할거에요 -> 전투 시작 전 상태로
isWeaponless = false; // 설정할거에요 -> 무기 소지 상태로
isPerformingAction = false; // 설정할거에요 -> 행동 전 상태로
IsAggroed = true; // 설정할거에요 -> 어그로 켠 상태로
mobRenderer = null; // 초기화할거에요 -> 렌더러 참조를
optimizationDistance = 9999f; // 설정할거에요 -> 최적화 거리를 무한대로
GameObject playerObj = GameObject.FindWithTag("Player"); // 찾을거에요 -> 플레이어 태그 오브젝트를
if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 플레이어를 찾았다면
{ // 코드 블록을 시작할거에요 -> 태그 탐색 성공 범위를
target = playerObj.transform; // 설정할거에요 -> 타겟 위치를
playerTransform = playerObj.transform; // 설정할거에요 -> 부모의 타겟 위치도
} // 코드 블록을 끝낼거에요 -> 태그 탐색 성공 끝
else // 조건이 틀리면 실행할거에요 -> 태그로 못 찾았다면
{ // 코드 블록을 시작할거에요 -> 타입 탐색 범위를
PlayerMovement playerScript = FindObjectOfType<PlayerMovement>(); // 찾을거에요 -> 스크립트 타입으로 플레이어를
if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 찾았다면
{ // 코드 블록을 시작할거에요 -> 타입 탐색 성공 범위를
target = playerScript.transform; // 설정할거에요 -> 타겟 위치를
playerTransform = playerScript.transform; // 설정할거에요 -> 부모의 타겟 위치도
} // 코드 블록을 끝낼거에요 -> 타입 탐색 성공 끝
} // 코드 블록을 끝낼거에요 -> 타입 탐색 끝
if (target == null) // 조건이 맞으면 실행할거에요 -> 여전히 못 찾았다면
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 찍을거에요
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
currentHP = maxHP; // 설정할거에요 -> 체력을 최대로
StartCoroutine(SafeInitNavMesh()); // 시작할거에요 -> 내비메시 안전 초기화를
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바 UI를
if (ballRb != null) ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니메이션을
} // 코드 블록을 끝낼거에요 -> Init을
private IEnumerator SafeInitNavMesh() // 코루틴을 정의할거에요 -> 내비메시 안전 초기화를
{ // 코드 블록을 시작할거에요 -> SafeInitNavMesh 범위를
yield return null; // 1프레임 쉴거에요
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
{ // 코드 블록을 시작할거에요 -> 에이전트 초기화 범위를
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 메시 위에 안 올라왔고 시도가 5번 미만이면
{ // 코드 블록을 시작할거에요 -> 재시도 루프 범위를
retry++; // 늘릴거에요 -> 횟수를
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초 동안
} // 코드 블록을 끝낼거에요 -> 재시도 루프 끝
if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 메시 위면 정지시킬거에요
agent.enabled = false; // 끌거에요 -> 에이전트를 우선
} // 코드 블록을 끝낼거에요 -> 에이전트 초기화 끝
} // 코드 블록을 끝낼거에요 -> SafeInitNavMesh를
protected override void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
{ // 코드 블록을 시작할거에요 -> Update 범위를
base.Update(); // 부모의 업데이트를 실행할거에요
if (!isDead) // 조건이 맞으면 실행할거에요 -> 살아있다면
{ // 코드 블록을 시작할거에요 -> AI 실행 범위를
ExecuteAILogic(); // 실행할거에요 -> AI 로직을
} // 코드 블록을 끝낼거에요 -> AI 실행 끝
} // 코드 블록을 끝낼거에요 -> Update를
protected override void ExecuteAILogic() // 함수를 정의할거에요 -> AI 행동 로직을
{ // 코드 블록을 시작할거에요 -> ExecuteAILogic 범위를
if (isPerformingAction || !isBattleStarted || target == null) return; // 중단할거에요 -> 행동 중이거나 시작 전이면
if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 무기가 없다면
{ // 코드 블록을 시작할거에요 -> 무기 회수 범위를
RetrieveWeaponLogic(); // 실행할거에요 -> 무기 회수 로직을
return; // 중단할거에요
} // 코드 블록을 끝낼거에요 -> 무기 회수 끝
if (isAttacking || isHit || isResting) return; // 중단할거에요 -> 공격/피격/휴식 중이면
if (_timer > 0f) // 조건이 맞으면 실행할거에요 -> 타이머가 남았다면
{ // 코드 블록을 시작할거에요 -> 타이머 감소 범위를
_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); // 바라볼거에요 -> 플레이어를
UpdateMovementAnimation(); // 실행할거에요 -> 이동 애니메이션 갱신을
} // 코드 블록을 끝낼거에요 -> 추격 끝
else // 조건이 틀리면 실행할거에요 -> 사거리 안이라면
{ // 코드 블록을 시작할거에요 -> 사거리 내 행동 범위를
agent.isStopped = true; // 멈출거에요 -> 이동을
agent.velocity = Vector3.zero; // 멈출거에요 -> 관성을 없앨거에요
LookAtTarget(target.position); // 바라볼거에요 -> 플레이어를
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면
{ // 코드 블록을 시작할거에요 -> 애니 상태 체크 범위를
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 상태를
if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷는 중이라면
{ // 코드 블록을 시작할거에요 -> 대기로 전환 범위를
animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
} // 코드 블록을 끝낼거에요 -> 대기로 전환 끝
} // 코드 블록을 끝낼거에요 -> 애니 상태 체크 끝
if (_timer <= 0f) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 끝났다면
{ // 코드 블록을 시작할거에요 -> 공격 시작 범위를
_timer = patternInterval; // 설정할거에요 -> 타이머를 다시
Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 찍을거에요
DecideAttack(); // 실행할거에요 -> 공격 결정 기능을
} // 코드 블록을 끝낼거에요 -> 공격 시작 끝
} // 코드 블록을 끝낼거에요 -> 사거리 내 행동 끝
} // 코드 블록을 끝낼거에요 -> ExecuteAILogic를
public void StartBossBattle() // 함수를 정의할거에요 -> 전투 시작 기능을
{ // 코드 블록을 시작할거에요 -> StartBossBattle 범위를
if (isBattleStarted) return; // 중단할거에요 -> 이미 시작했다면
StartCoroutine(BattleStartRoutine()); // 시작할거에요 -> 시작 연출 코루틴을
} // 코드 블록을 끝낼거에요 -> StartBossBattle를
private IEnumerator BattleStartRoutine() // 코루틴을 정의할거에요 -> 전투 시작 연출을
{ // 코드 블록을 시작할거에요 -> BattleStartRoutine 범위를
Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 찍을거에요
isPerformingAction = true; // 설정할거에요 -> 행동 수행 중으로
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 켤거에요 -> 체력바를
if (counterSystem != null) counterSystem.InitializeBattle(); // 실행할거에요 -> 카운터 시스템 초기화를
yield return StartCoroutine(PlayAndWait(anim_Roar, 2.0f)); // 대기할거에요 -> 포효 애니가 끝날 때까지
if (animator != null) animator.Play(anim_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
isBattleStarted = true; // 설정할거에요 -> 전투 시작 상태로
Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 찍을거에요
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트가 있다면
{ // 코드 블록을 시작할거에요 -> NavMesh 연결 범위를
agent.enabled = true; // 켤거에요 -> 에이전트를
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> 메시 위에 없고 시도가 10번 미만이면
{ // 코드 블록을 시작할거에요 -> 재시도 루프 범위를
retry++; // 늘릴거에요 -> 횟수를
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초 동안
} // 코드 블록을 끝낼거에요 -> 재시도 루프 끝
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 메시 위라면
{ // 코드 블록을 시작할거에요 -> 성공 처리 범위를
agent.isStopped = false; // 켤거에요 -> 이동을
Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 찍을거에요
} // 코드 블록을 끝낼거에요 -> 성공 처리 끝
else // 조건이 틀리면 실행할거에요 -> 연결 실패하면
{ // 코드 블록을 시작할거에요 -> 실패 처리 범위를
Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 찍을거에요
} // 코드 블록을 끝낼거에요 -> 실패 처리 끝
} // 코드 블록을 끝낼거에요 -> NavMesh 연결 끝
} // 코드 블록을 끝낼거에요 -> BattleStartRoutine를
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) // 조건이 맞으면 실행할거에요 -> 움직이고 있다면
{ // 코드 블록을 시작할거에요 -> 걷기 애니 처리 범위를
if (!state.IsName(anim_Walk)) animator.Play(anim_Walk); // 조건이 맞으면 재생할거에요 -> 걷기 애니를
} // 코드 블록을 끝낼거에요 -> 걷기 애니 처리 끝
else // 조건이 틀리면 실행할거에요 -> 멈춰 있다면
{ // 코드 블록을 시작할거에요 -> 대기 애니 처리 범위를
if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) animator.Play(anim_Idle); // 조건이 맞으면 재생할거에요 -> 대기 애니를
} // 코드 블록을 끝낼거에요 -> 대기 애니 처리 끝
} // 코드 블록을 끝낼거에요 -> UpdateMovementAnimation를
private void LookAtTarget(Vector3 targetPos) // 함수를 정의할거에요 -> 타겟 바라보기를
{ // 코드 블록을 시작할거에요 -> LookAtTarget 범위를
Vector3 dir = targetPos - transform.position; // 계산할거에요 -> 방향 벡터를
dir.y = 0f; // 무시할거에요 -> 높이 차이는
if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 있다면
{ // 코드 블록을 시작할거에요 -> 회전 처리 범위를
transform.rotation = Quaternion.Slerp( // 회전할거에요 -> 부드럽게
transform.rotation, // 현재 회전에서
Quaternion.LookRotation(dir), // 목표 회전으로
Time.deltaTime * 5f // 시간 속도에 맞춰서
); // 회전 계산을 끝낼거에요
} // 코드 블록을 끝낼거에요 -> 회전 처리 끝
} // 코드 블록을 끝낼거에요 -> LookAtTarget를
private void DecideAttack() // 함수를 정의할거에요 -> 공격 패턴 결정을
{ // 코드 블록을 시작할거에요 -> DecideAttack 범위를
if (target != null) LookAtTarget(target.position); // 조건이 맞으면 바라볼거에요 -> 타겟이 있다면
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 선택할거에요 -> 패턴 이름을
Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 찍을거에요
switch (patternName) // 분기할거에요 -> 패턴에 따라
{ // 코드 블록을 시작할거에요 -> switch 범위를
case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 실행할거에요 -> 돌진 공격을
case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 실행할거에요 -> 찍기 공격을
case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 실행할거에요 -> 휩쓸기 공격을
default: StartCoroutine(Pattern_ThrowBall()); break; // 실행할거에요 -> 공 던지기 공격을
} // 코드 블록을 끝낼거에요 -> switch 끝
} // 코드 블록을 끝낼거에요 -> DecideAttack를
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) // 조건이 맞으면 실행할거에요 -> 가까워지면
{ // 코드 블록을 시작할거에요 -> 줍기 코루틴 시작 범위를
StartCoroutine(PickUpBallRoutine()); // 시작할거에요 -> 줍기 코루틴을
} // 코드 블록을 끝낼거에요 -> 줍기 코루틴 시작 끝
} // 코드 블록을 끝낼거에요 -> RetrieveWeaponLogic를
private IEnumerator PickUpBallRoutine() // 코루틴을 정의할거에요 -> 무기 줍기 과정을
{ // 코드 블록을 시작할거에요 -> PickUpBallRoutine 범위를
OnAttackStart(); // 실행할거에요 -> 공격 시작 설정을
if (agent != null && agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> 에이전트가 정상이라면
{ // 코드 블록을 시작할거에요 -> 정지 처리 범위를
agent.isStopped = true; // 정지시킬거에요 -> 에이전트를
agent.velocity = Vector3.zero; // 멈출거에요 -> 속도를
} // 코드 블록을 끝낼거에요 -> 정지 처리 끝
yield return StartCoroutine(PlayAndWait(anim_Pickup, 1.5f)); // 대기할거에요 -> 줍기 애니 동안
ironBall.transform.SetParent(handHolder); // 설정할거에요 -> 쇠공의 부모를 손으로
ironBall.transform.localPosition = Vector3.zero; // 초기화할거에요 -> 상대 위치를
ironBall.transform.localRotation = Quaternion.identity; // 초기화할거에요 -> 상대 회전을
if (ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공 리지드바디가 있다면
{ // 코드 블록을 시작할거에요 -> 쇠공 고정 범위를
ballRb.isKinematic = true; // 설정할거에요 -> 쇠공 물리를 고정으로
ballRb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
} // 코드 블록을 끝낼거에요 -> 쇠공 고정 끝
isWeaponless = false; // 설정할거에요 -> 무기 소지 상태로
OnAttackEnd(); // 실행할거에요 -> 공격 종료 설정을
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 이동을 다시 켤거에요
} // 코드 블록을 끝낼거에요 -> PickUpBallRoutine를
public override void OnAttackStart() // 함수를 정의할거에요 -> 공격 시작 시 공통 처리를
{ // 코드 블록을 시작할거에요 -> OnAttackStart 범위를
isAttacking = true; // 설정할거에요 -> 공격 중 상태를
isPerformingAction = true; // 설정할거에요 -> 행동 수행 중 상태를
if (agent != null) agent.isStopped = true; // 멈출거에요 -> 에이전트를
} // 코드 블록을 끝낼거에요 -> OnAttackStart를
public override void OnAttackEnd() // 함수를 정의할거에요 -> 공격 종료 시 공통 처리를
{ // 코드 블록을 시작할거에요 -> OnAttackEnd 범위를
StartCoroutine(RecoverRoutine()); // 시작할거에요 -> 회복 코루틴을
} // 코드 블록을 끝낼거에요 -> OnAttackEnd를
private IEnumerator RecoverRoutine() // 코루틴을 정의할거에요 -> 공격 후 휴식 과정을
{ // 코드 블록을 시작할거에요 -> RecoverRoutine 범위를
isAttacking = false; // 해제할거에요 -> 공격 중 상태를
isResting = true; // 설정할거에요 -> 휴식 상태를
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 재생할거에요 -> 대기 애니를
yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 간격만큼
isResting = false; // 해제할거에요 -> 휴식 상태를
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
} // 코드 블록을 끝낼거에요 -> RecoverRoutine를
private IEnumerator Pattern_DashAttack() // 코루틴을 정의할거에요 -> 돌진 공격 과정을
{ // 코드 블록을 시작할거에요 -> Pattern_DashAttack 범위를
OnAttackStart(); // 설정할거에요 -> 공격 시작
if (agent != null) agent.enabled = false; // 끌거에요 -> 에이전트를
yield return StartCoroutine(PlayAndWait(anim_DashReady, 0.5f)); // 대기할거에요 -> 준비 애니 동안
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
{ // 코드 블록을 시작할거에요 -> 돌진 물리 처리 범위를
rb.isKinematic = false; // 설정할거에요 -> 물리를 켤거에요
rb.velocity = transform.forward * 20f; // 날아갈거에요 -> 전방 속도로
} // 코드 블록을 끝낼거에요 -> 돌진 물리 처리 끝
yield return StartCoroutine(PlayAndWait(anim_DashGo, 1.0f)); // 대기할거에요 -> 돌진 애니 동안
if (rb != null) // 조건이 맞으면 실행할거에요 -> 리지드바디가 있다면
{ // 코드 블록을 시작할거에요 -> 정지 처리 범위를
rb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
rb.isKinematic = true; // 설정할거에요 -> 물리를 다시 고정으로
} // 코드 블록을 끝낼거에요 -> 정지 처리 끝
if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 에이전트를 다시 켤거에요
OnAttackEnd(); // 설정할거에요 -> 공격 종료
} // 코드 블록을 끝낼거에요 -> Pattern_DashAttack를
private IEnumerator Pattern_SmashAttack() // 코루틴을 정의할거에요 -> 찍기 공격 과정을
{ // 코드 블록을 시작할거에요 -> Pattern_SmashAttack 범위를
OnAttackStart(); // 설정할거에요 -> 공격 시작
yield return StartCoroutine(PlayAndWait(anim_SmashImpact, 1.0f)); // 대기할거에요 -> 타격 애니 동안
OnAttackEnd(); // 설정할거에요 -> 공격 종료
} // 코드 블록을 끝낼거에요 -> Pattern_SmashAttack를
private IEnumerator Pattern_ShieldWall() // 코루틴을 정의할거에요 -> 휩쓸기 공격 과정을
{ // 코드 블록을 시작할거에요 -> Pattern_ShieldWall 범위를
OnAttackStart(); // 설정할거에요 -> 공격 시작
yield return StartCoroutine(PlayAndWait(anim_Sweep, 2.0f)); // 대기할거에요 -> 휩쓸기 애니 동안
OnAttackEnd(); // 설정할거에요 -> 공격 종료
} // 코드 블록을 끝낼거에요 -> Pattern_ShieldWall를
private IEnumerator Pattern_ThrowBall() // 코루틴을 정의할거에요 -> 공 던지기 과정을
{ // 코드 블록을 시작할거에요 -> Pattern_ThrowBall 범위를
OnAttackStart(); // 설정할거에요 -> 공격 시작
if (animator != null) animator.Play(anim_Throw, 0, 0f); // 재생할거에요 -> 던지기 애니를
yield return null; // 1프레임 쉴거에요
float throwClipLength = GetClipLength(anim_Throw, 1.5f); // 가져올거에요 -> 클립 길이를
float releaseTime = throwClipLength * 0.4f; // 계산할거에요 -> 발사 시점을
yield return new WaitForSeconds(Mathf.Max(0f, releaseTime - Time.deltaTime)); // 기다릴거에요 -> 발사 시점까지
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); // 던질거에요 -> 충격 힘으로
ballRb.angularDrag = 5f; // 설정할거에요 -> 회전 저항을
} // 코드 블록을 끝낼거에요 -> 쇠공 발사 끝
isWeaponless = true; // 설정할거에요 -> 무기 없음 상태를
float remainingTime = throwClipLength - releaseTime; // 계산할거에요 -> 남은 애니 시간을
if (remainingTime > 0f) yield return new WaitForSeconds(remainingTime); // 조건이 맞으면 기다릴거에요 -> 남은 시간만큼
OnAttackEnd(); // 설정할거에요 -> 공격 종료
} // 코드 블록을 끝낼거에요 -> Pattern_ThrowBall를
protected override void OnStartHit() // 함수를 정의할거에요 -> 피격 시 처리를
{ // 코드 블록을 시작할거에요 -> OnStartHit 범위를
// 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머) // 설명을 적을거에요 -> 의도적으로 비워둠
} // 코드 블록을 끝낼거에요 -> OnStartHit를
protected override void OnDie() // 함수를 정의할거에요 -> 사망 시 처리를
{ // 코드 블록을 시작할거에요 -> OnDie 범위를
isBattleStarted = false; // 설정할거에요 -> 전투 종료
isPerformingAction = false; // 해제할거에요 -> 행동 상태를
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
GiveBossXP(); // 실행할거에요 -> 경험치 지급을
base.OnDie(); // 부모의 사망 처리를 호출할거에요
} // 코드 블록을 끝낼거에요 -> OnDie를
private void GiveBossXP() // 함수를 정의할거에요 -> 경험치 지급 기능을
{ // 코드 블록을 시작할거에요 -> GiveBossXP 범위를
Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 찍을거에요 -> 지급 시도
if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 시스템 인스턴스가 있다면
{ // 코드 블록을 시작할거에요 -> 지급 범위를
ObsessionSystem.instance.AddRunXP(100); // 더할거에요 -> 경험치를 100만큼
} // 코드 블록을 끝낼거에요 -> 지급 끝
else // 조건이 틀리면 실행할거에요 -> 인스턴스가 없다면
{ // 코드 블록을 시작할거에요 -> 에러 처리 범위를
Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 찍을거에요
} // 코드 블록을 끝낼거에요 -> 에러 처리 끝
Debug.Log("<color=red>[BossController] 보스 처치 처리 완료!</color>"); // 로그를 찍을거에요 -> 빨간색으로
} // 코드 블록을 끝낼거에요 -> GiveBossXP를
} // 코드 블록을 끝낼거에요 -> NorcielBoss를