using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using System.Collections; // 코루틴 기능을 불러올거에요 -> IEnumerator를 // ============================================================ // NorcielBoss.FSM.cs — FSM 상태 핸들러 (partial class) // // [역할] // ① RunFSM — 현재 상태에 따라 적절한 핸들러 호출 // ② FSM_Chase — NavMesh 추격 + 카이팅/도망 감지 → 대시 // ③ FSM_PreAttack — 사거리 안 쿨타임 대기 + 뒤잡기 감지 + 패턴 실행 // ④ FSM_Retrieving — 쇠공 회수 이동 + 줍기 트리거 // ⑤ SetState — FSM 상태 전환 + 로그 // // [설계] // partial class로 NorcielBoss의 private 필드에 직접 접근. // Update()에서 RunFSM() 한 줄로 호출됩니다. // ============================================================ public partial class NorcielBoss : MonsterClass // 부분 클래스를 선언할거에요 -> NorcielBoss의 FSM 부분으로 { // ══════════════════════════════════════════════════════════ // FSM 메인 디스패처 // ══════════════════════════════════════════════════════════ /// 현재 FSM 상태에 따라 적절한 핸들러를 호출 private void RunFSM() // 함수를 선언할거에요 -> 현재 FSM 상태에 따라 행동을 처리하는 { if (!_isBattleStarted || _target == null) return; // 중단할거에요 -> 전투 전이거나 타겟 없으면 switch (_state) // 분기할거에요 -> 현재 상태에 따라 { case BossState.Chase: FSM_Chase(); break; // 실행할거에요 -> 추격 로직을 case BossState.PreAttack: FSM_PreAttack(); break; // 실행할거에요 -> 공격 대기 로직을 case BossState.Resting: /* 아무것도 안 함 */ break; // 코루틴이 처리하므로 -> 휴식 중 아무것도 하지 않음 case BossState.Retrieving: FSM_Retrieving(); break; // 실행할거에요 -> 쇠공 회수 로직을 // Idle / Attacking / Recovering / Dead → 코루틴이 처리하므로 여기서 아무것도 안 함 } } // ══════════════════════════════════════════════════════════ // Chase: NavMesh로 플레이어 추격 // // [우선순위] // 1. 쇠공 착지 → 회수로 전환 // 2. 카이팅 감지 → 대시 갭클로저 // 3. 공격 사거리 진입 → PreAttack // 4. 원거리 도망 감지 → 대시 추격 // 5. 일반 NavMesh 추격 // ══════════════════════════════════════════════════════════ private void FSM_Chase() // 함수를 선언할거에요 -> 추격 상태 처리를 { if (_isResting) return; // 중단할거에요 -> 휴식 중이면 (휴식 코루틴이 처리) // ── [v6.5] NavMesh 에이전트 안전 복구 ───────────────── // ChargeMonster 방식: 에이전트가 꺼져있으면 강제로 켜기 // 대시 후 복구가 불완전했을 때를 대비한 안전장치 if (agent != null && !agent.enabled) // 조건이 맞으면 실행할거에요 -> 에이전트가 꺼져있으면 { agent.enabled = true; // 켤거에요 -> NavMesh 에이전트를 강제로 (대시 후 미복구 대비) Debug.LogWarning("[Boss] FSM_Chase: agent.enabled=false 감지 → 강제 복구!"); // 경고를 찍을거에요 } // ── [v6.5] Rigidbody 안전 복구 ───────────────── // Chase 상태에서는 반드시 Kinematic이어야 해요 (NavMesh 이동 모드) // 대시 후 isKinematic=false가 남아있으면 미끄러짐 발생 if (_rb != null && !_rb.isKinematic) // 조건이 맞으면 실행할거에요 -> 물리 모드가 살아있으면 { _rb.velocity = Vector3.zero; // 멈출거에요 -> 잔여 속도를 _rb.isKinematic = true; // 끌거에요 -> 물리 연산을 강제로 Debug.LogWarning("[Boss] FSM_Chase: isKinematic=false 감지 → 강제 복구!"); // 경고를 찍을거에요 } if (!CheckNavMeshReady()) return; // 중단할거에요 -> NavMesh 비정상이면 float dist = GetDist(); // 가져올거에요 -> 거리를 // ── 1. 쇠공 착지 상태 → 회수 우선 ────────────── if (ironBall != null && ironBall.IsLanded()) // 조건이 맞으면 실행할거에요 -> 쇠공 착지 상태면 { StopAgent(); // 멈출거에요 SetState(BossState.Retrieving); // 전환할거에요 -> 회수로 return; // 중단할거에요 } // ── 2. 공격 사거리 진입 → PreAttack ────────────── if (dist <= throwRange) // 조건이 맞으면 실행할거에요 -> 사거리 안이면 { StopAgent(); // 멈출거에요 SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로 return; // 중단할거에요 } // ══════════════════════════════════════════════════════ // 3. [v8] 거리 기반 즉시 대시 추격 // // [v8 재설계] 기존 문제: // - dashCooldownTime=4초 동안 NavMesh 걷기 → 보스가 이상한 곳 돌아다님 // - 대시 → Recover → PreAttack → Chase 돌아올 때 쿨다운 남아있음 // - 4초 걷기 + 대시 + 4초 걷기 → 지루한 반복 // // v8 변경: 플레이어가 멀면 쿨다운 무시하고 즉시 대시! // - dist >= dashChaseRange(12m) → 무조건 즉시 대시 (쿨다운 체크 안 함) // - dist < dashChaseRange → 쿨다운 적용 (근거리에서는 대시 남발 방지) // → 멀리 도망가면 보스가 즉시 달려옴 = 지루함 해소 // → 가까이서는 적절한 간격 유지 = 게임성 보존 // ══════════════════════════════════════════════════════ // ── [v8] 원거리: 즉시 대시 (쿨다운 무시!) ────────────── // 플레이어가 dashChaseRange 이상 멀면 → 걷기 없이 바로 대시 // Recover 끝나고 돌아와도 즉시 다시 대시 → "이상한 곳 돌아다니기" 원천 차단 bool farEnough = dist >= dashChaseRange; // 판단할거에요 -> 대시 거리 이상인지를 bool cooldownReady = _dashCooldownTimer <= 0f; // 판단할거에요 -> 쿨다운 끝났는지를 bool shouldDash = farEnough || (dist >= meleeRange && cooldownReady); // 판단할거에요 -> 대시 발동 여부를 (멀면 무조건 / 중거리면 쿨다운 후) if (shouldDash && dist >= meleeRange) // 조건이 맞으면 실행할거에요 -> 대시 발동이면 { _dashCooldownTimer = dashCooldownTime; // 설정할거에요 -> 쿨다운을 (근거리 복귀 시 남발 방지용) _kitingTimer = 0f; // 초기화할거에요 -> 카이팅 타이머를 Debug.Log($"[Boss] v8 대시 추격! dist={dist:F1}m 원거리즉시={farEnough} 쿨다운통과={cooldownReady}"); // 로그를 찍을거에요 // Phase/상황에 따라 DashSmash 또는 Dash 선택 float dsmashRoll = Random.value; // 굴릴거에요 -> 확률 주사위를 float dsmashThreshold = (phaseManager != null) // 계산할거에요 -> DashSmash 확률을 ? phaseManager.GetDashSmashChance(dashSmashChance) // phaseManager에서 Phase별 확률 : dashSmashChance; // 기본값 if (dsmashRoll < Mathf.Clamp01(dsmashThreshold)) // 조건이 맞으면 실행할거에요 -> DashSmash 당첨이면 { Debug.Log("[Boss] → DashSmash로 추격!"); // 로그를 찍을거에요 StartCoroutine(Pattern_DashSmash()); // 실행할거에요 -> 대시+내려찍기를 } else // 아니면 { Debug.Log("[Boss] → Dash로 추격!"); // 로그를 찍을거에요 StartCoroutine(Pattern_Dash()); // 실행할거에요 -> 대시 돌진을 } return; // 중단할거에요 -> FSM_Chase를 } // ── 4. 일반 NavMesh 추격 ──────────────────────────── // 근접 사거리 안이거나, 대시 조건 미충족 시 걸어서 추격 // ── [버그 수정] NavMesh 자동회전 비활성화 ───────────── // 원인: agent.updateRotation=true + FaceTarget() 동시 실행 // → NavMesh 경로가 곡선일 때 agent가 옆/뒤를 바라보며 회전 // → FaceTarget()이 플레이어 방향으로 Slerp 충돌 → 빙빙 도는 현상 // 수정: updateRotation=false로 NavMesh 자동회전 차단 → FaceTarget()만 회전 담당 if (agent != null) agent.updateRotation = false; // 끌거에요 -> NavMesh 자동회전을 (FaceTarget이 회전 전담) agent.isStopped = false; // 켤거에요 -> 이동을 agent.SetDestination(_target.position); // 설정할거에요 -> 플레이어 위치로 FaceTarget(); // 바라볼거에요 -> 플레이어를 (updateRotation=false이므로 이것만 회전 담당) PlayMoveAnim(true); // 재생할거에요 -> 걷기 애니를 } // ══════════════════════════════════════════════════════════ // PreAttack: 사거리 안, 쿨타임 대기 후 패턴 실행 // // [우선순위] // 1. 쇠공 착지 → 회수 // 2. 사거리 이탈 → 추격 복귀 // 3. 카이팅 긴급 대응 → 대시 갭클로저 // 4. 뒤잡기 감지 → 긴급 Sweep // 5. 쿨타임 대기 중 접근 이동 // 6. 쿨타임 만료 → SelectAndFire // ══════════════════════════════════════════════════════════ private void FSM_PreAttack() // 함수를 선언할거에요 -> 공격 대기 상태 처리를 { if (_isResting) return; // 중단할거에요 -> 휴식 중이면 (휴식 코루틴이 처리) float dist = GetDist(); // 가져올거에요 -> 거리를 // ── 1. 쇠공 착지 상태 → 회수 우선 ────────────── if (ironBall != null && ironBall.IsLanded()) // 조건이 맞으면 실행할거에요 -> 쇠공 착지면 { SetState(BossState.Retrieving); // 전환할거에요 -> 회수로 return; // 중단할거에요 } // ── 2. 플레이어가 사거리 밖으로 나가면 → 다시 추격 ── if (dist > throwRange) // 조건이 맞으면 실행할거에요 -> 사거리 밖이면 { StartAgent(); // 켤거에요 -> NavMesh를 SetState(BossState.Chase); // 전환할거에요 -> 추격으로 return; // 중단할거에요 } // ── 3. [카이팅 긴급 대응] ─────────────────────── // [v6 수정] 직접 대시 발동 → 쿨타임 빠르게 소모로 변경 (이슈5: 대시 빈도 과다) // v5 문제: PreAttack에서 직접 Pattern_Dash/DashSmash 호출 → 대시 트리거가 5개나 되어 체감 빈도 과다 // v6 수정: 카이팅 감지 시 _attackTimer를 빠르게 단축 → SelectAndFire에서 자연스럽게 Dash 선택되게 유도 // → 대시 발동 경로 1개 제거 (5개→4개, Throw후 제거 포함하면 3개) // → SelectAndFire의 가중치 시스템이 카이팅 상황에서 알아서 Dash 가중치를 높여줌 bool preAttackKiting = _kitingTimer >= kitingDetectTime && dist > meleeRange; // 판단할거에요 -> PreAttack 카이팅 발동 조건을 (v6: 대시쿨다운 체크 제거 — 직접 대시 안 하니까) if (preAttackKiting) // 조건이 맞으면 실행할거에요 -> 카이팅 감지면 { Debug.Log($"[Boss] PreAttack 카이팅 감지 → 쿨타임 단축! (dist={dist:F1}m, _attackTimer → 0.3s로 단축)"); // 로그를 찍을거에요 _attackTimer = Mathf.Min(_attackTimer, 0.3f); // 단축할거에요 -> 공격 쿨타임을 최대 0.3초로 (빠르게 SelectAndFire 도달) _kitingTimer = 0f; // 초기화할거에요 -> 카이팅 타이머를 리셋 (중복 감지 방지) // v6: 직접 대시 발동 안 함 → SelectAndFire가 카이팅 상황에서 Dash/DashSmash 가중치를 올려줌 } // ── 4. [반응형] 뒤잡기 감지 → 긴급 Sweep ────────── if (counterSystem != null && counterSystem.ShouldReactToBehind() && _state == BossState.PreAttack) // 조건이 맞으면 실행할거에요 -> 뒤잡기 반응 필요하면 { counterSystem.ResetBehindTimer(); // 초기화할거에요 -> 뒤잡기 타이머를 Debug.Log("[Boss] 뒤잡기 감지 → 긴급 Sweep!"); // 로그를 찍을거에요 StartCoroutine(Pattern_Sweep()); // 시작할거에요 -> 긴급 휩쓸기를 return; // 중단할거에요 -> PreAttack을 } FaceTarget(); // 바라볼거에요 -> 플레이어를 _attackTimer -= Time.deltaTime; // 줄일거에요 -> 쿨타임을 // ── 5. 쿨타임 대기 중 이동 판단 ───────────────── // [v5 개선] 기본 정지 거리를 Smash 실제 타격 범위 기준으로 수정 // // 기존 문제 (v3): midStopRange = 6.75m → Smash 범위(5.5m) 밖 // → 보스가 6.75m에서 멈추고 Smash/Sweep 가중치 0 → Dash/DashSmash만 선택 → 반복 // → "원거리에서 제자리 공격" 현상 발생 // // 수정: 기본 정지 거리를 actualSmashRange(5.5m) 이내로 설정 // → 보스가 Smash 타격 범위 안에서 멈추므로 근접 패턴 선택 가능 // → 인스펙터에서 smashRadius/smashForwardOffset 조정 시 자동 반영 if (_attackTimer > 0f) // 조건이 맞으면 실행할거에요 -> 아직 쿨타임 중이면 { // [v5] actualSmashRange 기반 정지 거리 — Smash가 닿는 거리에서 멈추게 float actualSmashRange = smashForwardOffset + smashRadius + 1f; // 계산할거에요 -> Smash 실제 타격 최대 거리를 (1.5+3+1=5.5m) float midStopRange = Mathf.Min(actualSmashRange - 0.5f, (meleeRange + throwRange) * 0.5f); // 계산할거에요 -> 정지 거리를 (v5: Smash 범위 안으로, 기존 중간 거리보다 가까이) // [v5] 플레이어가 정지 + 피격 OR 쿨타임 중 접근 필요 → 근접까지 접근 bool playerCamping = _playerStillTimer >= stillPunishTime && _recentHitCount >= 1; // 판단할거에요 -> 플레이어 캠핑 여부를 (정지 + 보스 피격 중) float approachTarget = playerCamping ? meleeRange : midStopRange; // 결정할거에요 -> 접근 목표를 (캠핑이면 근접, 아니면 Smash 범위 내) if (dist <= meleeRange) // 조건이 맞으면 실행할거에요 -> 이미 근접 사거리 안이면 { StopAgent(); // 멈출거에요 -> NavMesh를 PlayMoveAnim(false); // 재생할거에요 -> 대기 애니를 } else if (dist > approachTarget) // 조건이 맞으면 실행할거에요 -> 목표 거리보다 멀면 { if (CheckNavMeshReady()) // 조건이 맞으면 실행할거에요 -> NavMesh 정상이면 { // ── [버그 수정] PreAttack 이동 중 자동회전 차단 ── // Chase와 동일한 원인: NavMesh 곡선 경로 → 자동회전 충돌 → 빙빙 돔 agent.updateRotation = false; // 끌거에요 -> NavMesh 자동회전을 (FaceTarget이 회전 전담) agent.isStopped = false; // 켤거에요 -> 이동을 Vector3 dirToPlayer = (_target.position - transform.position).normalized; // 계산할거에요 -> 플레이어 방향을 Vector3 stopDest = _target.position - dirToPlayer * approachTarget; // 계산할거에요 -> 목표 지점을 (캠핑: 근접 / 일반: 중거리) agent.SetDestination(stopDest); // 설정할거에요 -> 목표 지점으로 이동 } PlayMoveAnim(true); // 재생할거에요 -> 걷기 애니를 } else // 목표 거리 이내에 도착했으면 { StopAgent(); // 멈출거에요 -> NavMesh를 PlayMoveAnim(false); // 재생할거에요 -> 대기 애니를 } return; // 중단할거에요 -> 쿨타임 끝날 때까지 } // ── 6. 쿨타임 만료 → 현재 dist 기반 패턴 실행 ────── StopAgent(); // 멈출거에요 -> NavMesh를 (패턴 실행 전 정지) PlayMoveAnim(false); // 재생할거에요 -> 대기 애니를 _attackTimer = patternInterval + Random.Range(-intervalVariance, intervalVariance); // 재설정할거에요 -> 쿨타임을 (±분산 적용) SelectAndFire(dist); // 실행할거에요 -> 현재 dist 기반 패턴 선택 + 실행 } // ══════════════════════════════════════════════════════════ // Retrieving: 쇠공 위치로 이동 후 줍기 // ══════════════════════════════════════════════════════════ private void FSM_Retrieving() // 함수를 선언할거에요 -> 쇠공 회수 상태 처리를 { // [D-5 수정] Retrieving 진입 시 updateRotation 명시적 복원 // 이전 패턴(Dash/BossRest 등)에서 false로 설정된 채 남아있을 수 있음 // → true로 되돌려야 FaceTarget(ironBall)이 정상 회전을 가져감 if (agent != null) agent.updateRotation = true; // 복원할거에요 -> NavMesh 자동회전을 (이전 패턴에서 꺼진 채 남아있을 수 있음) if (ironBall == null || !ironBall.IsLanded()) // 조건이 맞으면 실행할거에요 -> 쇠공 없거나 착지 안 됐으면 { SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로 return; // 중단할거에요 } // ── 도달 불가능 높이 체크 ────────────────────────── float ballHeightDiff = ironBall.transform.position.y - transform.position.y; // 계산할거에요 -> 높이 차이를 if (ballHeightDiff > 3f) // 조건이 맞으면 실행할거에요 -> 3m 이상 위면 { Debug.LogWarning($"[Boss] 쇠공이 도달 불가능한 높이에요! (높이차={ballHeightDiff:F1}m) → 강제 회수!"); // 경고를 찍을거에요 ironBall.ForceReturn(handHolder); // 실행할거에요 -> 강제 회수를 SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로 return; // 중단할거에요 } if (!CheckNavMeshReady()) return; // 중단할거에요 -> NavMesh 비정상이면 agent.isStopped = false; // 켤거에요 -> 이동을 agent.SetDestination(ironBall.transform.position); // 이동할거에요 -> 쇠공 위치로 PlayMoveAnim(true); // 재생할거에요 -> 걷기 애니를 FaceTarget(ironBall.transform.position); // 바라볼거에요 -> 쇠공을 // 충분히 가까우면 줍기 코루틴 시작 float distToBall = Vector3.Distance(transform.position, ironBall.transform.position); // 계산할거에요 -> 쇠공까지 거리를 if (distToBall <= 2.0f) // 조건이 맞으면 실행할거에요 -> 2m 이내면 { SetState(BossState.Attacking); // 잠글거에요 -> 중복 진입 방지 StartCoroutine(PickupBall()); // 시작할거에요 -> 줍기 코루틴을 } } // ══════════════════════════════════════════════════════════ // FSM 상태 변경 유틸리티 // ══════════════════════════════════════════════════════════ /// FSM 상태를 변경하고 디버그 로그를 출력 private void SetState(BossState s) // 함수를 선언할거에요 -> FSM 상태를 변경하는 { if (_state == s) return; // 중단할거에요 -> 같은 상태면 중복 설정 방지 _state = s; // 설정할거에요 -> 새 상태로 Debug.Log($"[BossFSM] → {s}"); // 로그를 찍을거에요 } }