Projext/Assets/02_Scripts/Enemy/BossAI/NorcielBoss.cs

805 lines
60 KiB
C#
Raw Normal View History

using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
using System.Collections; // 코루틴 기능을 불러올거에요 -> IEnumerator를
// ============================================================
// NorcielBoss.cs ─ 보스 AI 메인 스크립트 (v3 재작성)
//
// [설계 원칙]
// ① 보스는 NavMesh + Rigidbody 둘 다 사용
// - 이동/추격: NavMesh
// - 대시: Rigidbody velocity (나중에 구현)
// ② 쇠공 물리/데미지는 BossIronBall.cs 가 전담
// - 이 스크립트는 ironBall.Launch(targetPos) 한 줄만 호출
// ③ FSM(유한 상태 머신)으로 상태 명확히 관리
// ④ 애니메이션 이벤트는 선택적 (없으면 타이머 폴백으로 진행)
//
// [지금 구현된 것]
// - 던지기 패턴 (쇠공 포물선 발사 + 줍기)
// - Phase2 진입 (HP 50% → 포효 + 스탯 강화)
// - FSM 기반 추격 / 공격 대기 / 회수 상태
//
// [나중에 추가할 것] ← 스텁(stub)으로 자리만 잡아둠
// - 내려찍기 (Pattern_Smash)
// - 휩쓸기 (Pattern_Sweep)
// - 대시 추격 (Pattern_Dash)
// - Phase2 콤보 패턴
//
// [애니메이션 이벤트 등록 (선택사항)]
// Attack_Throw → 공 손 떠나는 프레임: OnThrowRelease
// 모든 공격 클립 → 마지막 프레임 : OnAnimEnd
// ============================================================
public partial class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를 (partial 분할 정의)
{
// ══════════════════════════════════════════════════════════
// FSM 상태 / 패턴 열거형
// → BossEnums.cs로 이전됨 (BossState, BossPattern)
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// Inspector — 연결
// ══════════════════════════════════════════════════════════
[Header("=== 연결 ===")]
[Tooltip("BossIronBall 스크립트가 부착된 쇠공 오브젝트")]
[SerializeField] private BossIronBall ironBall; // 변수를 선언할거에요 -> 쇠공 스크립트를
[Tooltip("쇠공이 붙을 손 위치 Transform (손 본 또는 Empty 오브젝트)")]
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 손 Transform을
[Tooltip("보스 체력바 UI 오브젝트")]
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 체력바 UI를
[Tooltip("카운터 패턴 결정 시스템 (없어도 작동)")]
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 카운터 시스템을
[Tooltip("페이즈 관리 컴포넌트 (Phase2/3 전환, 스탯 배율)")]
[SerializeField] private BossPhaseManager phaseManager; // 변수를 선언할거에요 -> 페이즈 관리 컴포넌트를
[Tooltip("디버그 표시 컴포넌트 (기즈모, OnGUI, 테스트 버튼)")]
[SerializeField] private BossDebugDisplay debugDisplay; // 변수를 선언할거에요 -> 디버그 표시 컴포넌트를
[Tooltip("잡몹 소환 시스템 (HP 구간별 소환, 없어도 작동)")]
[SerializeField] private BossSummonSystem summonSystem; // 변수를 선언할거에요 -> 잡몹 소환 시스템을 (없으면 소환 안 함)
[Tooltip("보스방 트리거존 (사망 시 벽 해제용, 없어도 작동)")]
[SerializeField] private BossZoneTrigger zoneTrigger; // 변수를 선언할거에요 -> 트리거존 참조를 (사망 시 벽 열기)
[Tooltip("범위 공격 바닥 표시기 (없어도 작동)")]
[SerializeField] private BossAttackIndicator attackIndicator; // 변수를 선언할거에요 -> 범위 공격 표시기를 (Smash/Sweep 바닥 빨간 원)
// ══════════════════════════════════════════════════════════
// Inspector — 거리 기준
// ══════════════════════════════════════════════════════════
[Header("=== 보스 크기 조절 ===")]
[Tooltip("보스 스케일 배율 (1.0 = 원본 크기)\n" +
"애니메이션 콜라이더가 플레이어를 밀어내는 문제가 있으면 줄여보세요\n" +
"추천: 0.7~0.9 (원본보다 약간 작게)")]
[Range(0.3f, 2f)]
[SerializeField] private float bossScale = 1.0f; // 변수를 선언할거에요 -> 보스 크기 배율을 (1.0=원본 크기, 필요 시 Inspector에서 조절)
[Header("=== 거리 기준 (씬 뷰 기즈모로 확인 가능) ===")]
[Tooltip("이 거리 이하 → 근접 패턴 (찍기/휩쓸기)\n현재는 미구현 → 던지기로 대체")]
[SerializeField] private float meleeRange = 3.5f; // 변수를 선언할거에요 -> 근접 사거리를
[Tooltip("이 거리 이하 → 던지기 패턴\n이 거리 초과 → NavMesh 추격")]
[SerializeField] private float throwRange = 10f; // 변수를 선언할거에요 -> 던지기 사거리를
[Tooltip("[v6.5] (ChargeMonster )\n" +
"throwRange보다 약간 높게 설정 → 사거리 벗어나면 바로 돌진\n" +
"추천: throwRange + 2~4m (사거리 벗어나자마자 반응)")]
[SerializeField] private float dashChaseRange = 12f; // [v6.5] 변수를 선언할거에요 -> 대시 추격 발동 거리를 (이 거리 이상 + 쿨다운 끝남 → 즉시 대시)
[Tooltip("[v6.5] ()\n" +
"ChargeMonster의 chargeDelay와 동일한 역할\n" +
"추천: 3~5초 (낮으면 연속 대시, 높으면 여유로움)")]
[SerializeField] private float dashCooldownTime = 4f; // [v6.5] 변수를 선언할거에요 -> 대시 쿨다운을 (v6.5: 6→4초 단축, ChargeMonster의 chargeDelay=3과 유사)
[Tooltip("[v6.5] \n" +
"ChargeMonster 방식: 매 프레임 Slerp 회전으로 자연스럽게 추적\n" +
"0이면 직선 대시 (방향 고정), 높을수록 예리하게 추적\n" +
"추천: 2~5 (너무 높으면 회피 불가, 너무 낮으면 빗나감)")]
[SerializeField] private float dashTrackingSpeed = 3f; // [v6.5] 변수를 선언할거에요 -> 대시 중 실시간 추적 회전 속도를 (ChargeMonster 방식, 0이면 직선 고정)
[Tooltip("카이팅 감지 필요 시간 (초)\n" +
"플레이어가 이 시간 이상 거리를 유지하며 공격해야 카이팅으로 판정\n" +
"추천: 2~4초 (낮으면 민감, 높으면 둔감)")]
[SerializeField] private float kitingDetectTime = 2.5f; // 변수를 선언할거에요 -> 카이팅 판정 기준 시간을 (기본 2.5초, 기존 1초에서 상향)
// ══════════════════════════════════════════════════════════
// Inspector — 타이밍
// ══════════════════════════════════════════════════════════
[Header("=== 타이밍 ===")]
[Tooltip("공격 패턴 사이 대기 시간 (초)\n" +
"[v6: 3.0 → 1.8] 인식→패턴 발동 딜레이 단축\n" +
"기존 3초는 보스가 멍하니 서있는 느낌 → 1.8초로 긴장감 유지\n" +
"인스펙터에서 1.5~2.5 사이로 미세 조정 권장")]
[SerializeField] private float patternInterval = 1.8f; // 변수를 선언할거에요 -> 공격 간격을 (v6: 3.0→1.8 단축, 루즈한 느낌 해소)
[Tooltip("던지기 발사 타이밍 (클립 길이 비율, 0.3~0.6 권장)")]
[SerializeField][Range(0.1f, 0.9f)] private float throwTiming = 0.4f; // 변수를 선언할거에요 -> 던지기 발사 타이밍을
[Tooltip("줍기: 손 본과 쇠공 사이 이 거리 이하 → 실시간 부착 (0.5~1.0 권장)\n현재는 BeginPickup 방식으로 미사용 (하위 호환용으로 보존)")]
[SerializeField] private float grabDistance = 0.8f; // 변수를 선언할거에요 -> (레거시) 실시간 거리 감지 임계값을 (현재 BeginPickup 방식으로 미사용)
[Tooltip("줍기 애니 시작 후 공을 손에 부착하기까지의 지연 시간 (초)\n" +
"→ 줍기 애니에서 손이 공에 가장 가까워지는 프레임 타이밍에 맞춰 조정하세요\n" +
"예) 줍기 클립이 1.2초이고 손이 0.5초 지점에서 공에 닿으면 → 0.5로 설정\n" +
"0이면 애니 시작 즉시 부착 (기존 동작)")]
[Range(0f, 3f)]
[SerializeField] private float pickupGrabDelay = 0.4f; // 변수를 선언할거에요 -> 줍기 애니에서 손이 공에 닿는 타이밍까지의 딜레이를 (Inspector에서 조정)
[Tooltip("쇠공 조준 높이 오프셋\n0.0 = 발 / 0.7 = 복부 / 1.0 = 가슴 / 1.5 = 머리\n높을수록 공이 상체에 명중")]
[Range(0f, 1.8f)]
[SerializeField] private float throwAimHeight = 0.7f; // 변수를 선언할거에요 -> 쇠공이 조준할 플레이어 몸통 높이를
// ══════════════════════════════════════════════════════════
// Phase2/3 설정, 콤보 체인, 경직/쿨타임
// → BossPhaseManager.cs로 이전됨 (phaseManager 참조)
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 타이밍 랜덤 분산 — 고정 타이밍 방지
// 모든 대기 시간에 ±variance 범위의 랜덤을 더해요.
// 플레이어가 타이밍을 암기할 수 없게 만들어줘요.
// ══════════════════════════════════════════════════════════
[Header("=== 타이밍 랜덤 분산 ===")]
[Tooltip("patternInterval에 더해지는 랜덤 범위 (±초)\n" +
"[v6: 0.5 → 0.3] 랜덤 폭 축소로 반응 일관성 향상\n" +
"예: 0.3 → 쿨타임 1.5~2.1초로 변동")]
[SerializeField] private float intervalVariance = 0.3f; // 변수를 선언할거에요 -> 쿨타임 랜덤 분산 범위를 (v6: 0.5→0.3 축소)
[Tooltip("경직 시간에 더해지는 랜덤 범위 (±초)")]
[SerializeField] private float recoverVariance = 0.15f; // 변수를 선언할거에요 -> 경직 랜덤 분산 범위를
[Tooltip("와인드업에 더해지는 랜덤 범위 (±초)\n예: 0.15 → 와인드업 0.25~0.55초로 변동")]
[SerializeField] private float windupVariance = 0.15f; // 변수를 선언할거에요 -> 와인드업 랜덤 분산 범위를
// ══════════════════════════════════════════════════════════
// 플레이어 행동 감지 — 적응형 AI
// ══════════════════════════════════════════════════════════
[Header("=== 플레이어 행동 감지 ===")]
[Tooltip("플레이어 이동 속도가 이 값 이하면 '정지'로 판단 (m/s)")]
[SerializeField] private float playerStillThreshold = 0.5f; // 변수를 선언할거에요 -> 정지 판단 속도 임계값을
[Tooltip("플레이어가 이 시간(초) 이상 정지하면 공격적 패턴 선택")]
[SerializeField] private float stillPunishTime = 1.5f; // 변수를 선언할거에요 -> 정지 처벌 시간을
[Tooltip("플레이어가 접근 중일 때 이 시간(초) 이상이면 Sweep/Smash 우선")]
[SerializeField] private float approachReactTime = 1.0f; // 변수를 선언할거에요 -> 접근 감지 반응 시간을
// ══════════════════════════════════════════════════════════
// 반응형 행동 / 패턴 반복 방지 / 숨돌리기
// → BossCounterSystem.cs로 이전됨 (counterSystem 참조)
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// Inspector — 애니메이션 State 이름
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// Inspector — 애니메이션 State 이름 (anim_Idle, anim_Walk 등)
// → NorcielBoss.Animation.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// Inspector — 패턴 테스트 버튼
// → BossDebugDisplay.cs로 이전됨 (debugDisplay 참조)
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 런타임 상태 변수
// ══════════════════════════════════════════════════════════
private BossState _state = BossState.Idle; // 변수를 선언할거에요 -> 현재 FSM 상태를
private Transform _target; // 변수를 선언할거에요 -> 공격 대상 Transform을
private bool _isBattleStarted = false; // 변수를 선언할거에요 -> 전투 시작 여부를
// ── Phase 상태 → phaseManager에서 관리 ──
// 패턴 코루틴에서의 편의 접근용 프로퍼티
private bool _isPhase2 => phaseManager != null && phaseManager.IsPhase2; // 속성을 선언할거에요 -> Phase2 여부를 phaseManager에서 읽기
private bool _isPhase3 => phaseManager != null && phaseManager.IsPhase3; // 속성을 선언할거에요 -> Phase3 여부를 phaseManager에서 읽기
private int _comboCounter = 0; // 변수를 선언할거에요 -> 현재 연속 콤보 횟수를 (maxComboCount 도달 시 강제 경직)
private float _attackTimer = 0f; // 변수를 선언할거에요 -> 공격 쿨타임 타이머를
private float _dashCooldownTimer = 0f; // 변수를 선언할거에요 -> 대시 쿨다운 남은 시간을 (0이면 대시 가능, ChargeMonster의 lastChargeTime 역할)
// ── 패턴 AI 고도화 변수 ──────────────────────────────
private BossPattern _lastPattern = (BossPattern)(-1); // 변수를 선언할거에요 -> 직전에 사용한 패턴을 (연속 방지용, -1 = 아직 없음)
private int _samePatternCount = 0; // 변수를 선언할거에요 -> 같은 패턴 연속 사용 횟수를
// ── 반응형/히스토리/숨돌리기 → counterSystem에서 관리 ──
private bool _isResting = false; // 변수를 선언할거에요 -> 휴식 중 여부를 (BossRest 코루틴 로컬 플래그)
private bool _phaseTransitioning = false; // 변수를 선언할거에요 -> Phase 전환 진행 중 여부를 (D-4: 중복 발동 방지)
private float _targetRefreshTimer = 0f; // 변수를 선언할거에요 -> 플레이어 캐시 갱신 타이머를 (D-3: 3초마다 재탐색)
// ══════════════════════════════════════════════════════════
// 초기화
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 근접 패턴 공용 설정 (내려찍기 / 휩쓸기)
// ══════════════════════════════════════════════════════════
[Header("=== 내려찍기 (Pattern_Smash) ===")]
[Tooltip("내려찍기 판정 중심 오프셋 (보스 앞방향 거리)")]
[SerializeField] private float smashForwardOffset = 1.5f; // 변수를 선언할거에요 -> 판정 중심 오프셋을 (보스 앞 몇 m)
[Tooltip("내려찍기 판정 반경 (m)")]
[SerializeField] private float smashRadius = 3f; // 변수를 선언할거에요 -> 내려찍기 반경을
[Tooltip("내려찍기 데미지")]
[SerializeField] private float smashDamage = 30f; // 변수를 선언할거에요 -> 내려찍기 데미지를
[Tooltip("내려찍기 손/무기가 바닥에 닿는 프레임 타이밍 (초)\n" +
"→ Unity Animation 창에서 임팩트 프레임 확인 후 입력\n" +
"계산법: 임팩트 프레임 ÷ 클립 FPS = 초\n" +
"예) 60fps 클립의 130프레임 → 130 ÷ 60 = 2.17")]
[Range(0.05f, 6f)]
[SerializeField] private float smashTiming = 2.17f; // 변수를 선언할거에요 -> 내려찍기 임팩트 타이밍을 (초 단위, 프레임 ÷ fps로 계산)
[Tooltip("내려찍기 피격 시 플레이어를 수평으로 날리는 넉백 힘 (m/s)\n" +
"0이면 넉백 없음 / 추천: 8~14 (묵직한 강타 느낌)")]
[SerializeField] private float smashKnockbackForce = 10f; // 변수를 선언할거에요 -> 내려찍기 수평 넉백 세기를 (크면 멀리 날아감)
[Tooltip("내려찍기 피격 시 플레이어를 위로 튀기는 힘 (m/s)\n" +
"0이면 수평만 / 추천: 4~8 (찍혀서 공중에 뜨는 느낌)")]
[SerializeField] private float smashKnockbackUpForce = 5f; // 변수를 선언할거에요 -> 내려찍기 수직 넉백 세기를 (크면 높이 뜸)
[Tooltip("내려찍기 넉백 + 경직 지속 시간 (초)\n" +
"추천: 0.5~0.8 (짧으면 빠른 회복, 길면 그로기 느낌)")]
[SerializeField] private float smashKnockbackDuration = 0.6f; // 변수를 선언할거에요 -> 내려찍기 넉백 지속 시간을 (이 시간 동안 이동 불가)
[Header("=== 스매시 흡인 효과 (Suction) ===")]
[Tooltip("흡인 효과 작동 범위 (m)\n" +
"이 반경 안에 있는 플레이어만 끌어당김\n" +
"추천: smashRadius + 3~5")]
[SerializeField] private float smashSuctionRange = 8f; // 변수를 선언할거에요 -> 흡인 효과 범위를 (이 거리 안의 플레이어를 당김)
[Tooltip("초당 끌어당기는 속도 (m/s)\n" +
"너무 크면 회피 불가 → 2~4 추천\n" +
"거리가 가까울수록 자동으로 강해짐")]
[SerializeField] private float smashSuctionForce = 5f; // 변수를 선언할거에요 -> 흡인 속도를 (초당 몇 m씩 끌어당길지) [v6.3: 3→5 강화]
[Tooltip("흡인 시작 시점 (smashTiming 대비 비율)\n" +
"0.0 = 애니 시작과 동시에 / 0.3 = 30% 지점부터\n" +
"추천: 0.1~0.3 (와인드업 직후부터)")]
[Range(0f, 0.8f)]
[SerializeField] private float smashSuctionStartRatio = 0.15f; // 변수를 선언할거에요 -> 흡인 시작 비율을 (smashTiming의 15% 시점부터)
[Tooltip("흡인 종료 시점 (smashTiming 대비 비율)\n" +
"추천: 0.7~0.9 (임팩트 직전에 흡인 종료 → 타격 직전 약간의 틈 부여)")]
[Range(0.2f, 1f)]
[SerializeField] private float smashSuctionEndRatio = 0.75f; // 변수를 선언할거에요 -> 흡인 종료 비율을 (smashTiming의 75% 시점에 끝)
private PlayerMovement _cachedPlayerMovement; // 변수를 선언할거에요 -> 캐싱된 플레이어 이동 컴포넌트를 (매번 GetComponent 방지)
[Header("=== 휩쓸기 (Pattern_Sweep) ===")]
[Tooltip("휩쓸기 판정 반경 (m)")]
[SerializeField] private float sweepRadius = 4f; // 변수를 선언할거에요 -> 휩쓸기 반경을
[Tooltip("휩쓸기 각도 범위 (좌우 합산, 180 = 전방 반원)")]
[SerializeField] private float sweepAngle = 180f; // 변수를 선언할거에요 -> 휩쓸기 각도 범위를
[Tooltip("휩쓸기 데미지")]
[SerializeField] private float sweepDamage = 20f; // 변수를 선언할거에요 -> 휩쓸기 데미지를
[Tooltip("휩쓸기 팔이 플레이어 쪽을 통과하는 타이밍 (초)\n" +
"→ Unity Animation 창에서 타격 프레임 확인 후 입력\n" +
"계산법: 타격 프레임 ÷ 클립 FPS = 초\n" +
"예) 60fps 클립의 90프레임 → 90 ÷ 60 = 1.5")]
[Range(0.05f, 6f)]
[SerializeField] private float sweepTiming = 1.5f; // 변수를 선언할거에요 -> 휩쓸기 타격 타이밍을 (초 단위, 프레임 ÷ fps로 계산)
[Tooltip("휩쓸기 피격 시 플레이어를 수평으로 날리는 넉백 힘 (m/s)\n" +
"0이면 넉백 없음 / 추천: 8~15 (팔에 쓸려나가는 느낌)")]
[SerializeField] private float sweepKnockbackForce = 12f; // 변수를 선언할거에요 -> 휩쓸기 수평 넉백 세기를 (크면 멀리 날아감)
[Tooltip("휩쓸기 피격 시 플레이어를 위로 띄우는 힘 (m/s)\n" +
"0이면 수평만 / 추천: 2~5 (살짝 뜨는 느낌)")]
[SerializeField] private float sweepKnockbackUpForce = 3f; // 변수를 선언할거에요 -> 휩쓸기 수직 넉백 세기를 (크면 높이 뜸)
[Tooltip("휩쓸기 넉백 + 경직 지속 시간 (초)\n" +
"추천: 0.4~0.7 (짧으면 빠른 회복, 길면 그로기 느낌)")]
[SerializeField] private float sweepKnockbackDuration = 0.5f; // 변수를 선언할거에요 -> 휩쓸기 넉백 지속 시간을 (이 시간 동안 이동 불가)
[Tooltip("근접 시 휩쓸기를 선택할 확률 (0~1)\n" +
"0 = 항상 내려찍기 / 0.5 = 50:50 / 1 = 항상 휩쓸기\n" +
"카운터 시스템이 연결돼 있으면 이 값은 무시돼요")]
[Range(0f, 1f)]
[SerializeField] private float sweepSelectChance = 0.5f; // 변수를 선언할거에요 -> 근접 시 Sweep를 선택할 확률을 (0~1)
[Header("=== 대시 추격 (Pattern_Dash) ===")]
[Tooltip("대시 직전 예비 모션 지속 시간 (초)\n" +
"이 시간 동안 멈추고 플레이어 방향을 조준해요 → 플레이어에게 회피 기회 부여\n" +
"0이면 즉시 대시 / 추천: 0.3~0.6")]
[Range(0f, 1.5f)]
[SerializeField] private float dashWindupTime = 0.4f; // 변수를 선언할거에요 -> 대시 예비 모션 시간을 (이 시간 동안 멈추고 플레이어 조준)
[Tooltip("대시 속도 (m/s)\n" +
"[v5 수정] 18 → 11 부자연스러운 순간이동 느낌 완화\n" +
"인스펙터에서 10~14 사이로 조절 권장")]
[SerializeField] private float dashSpeed = 11f; // 변수를 선언할거에요 -> 대시 속도를 (v5: 18 → 11 하향, 인스펙터에서 미세 조정)
[Tooltip("대시 지속 시간 (초)\n짧을수록 빠르게 끊김 / 추천: 0.4~0.8")]
[SerializeField] private float dashDuration = 0.5f; // 변수를 선언할거에요 -> 대시 지속 시간을 (이 시간 동안 dashDir 방향으로 대시)
[Tooltip("대시 충돌 데미지")]
[SerializeField] private float dashDamage = 25f; // 변수를 선언할거에요 -> 대시 충돌 데미지를
[Tooltip("대시 중 플레이어까지 거리가 이 값 이하면 충돌 데미지 발동 (m)\n매 프레임 Vector3.Distance로 체크 / 추천: 1.5~2.5")]
[SerializeField] private float dashHitRange = 1.8f; // 변수를 선언할거에요 -> 대시 충돌 판정 거리를 (이 거리 이하 진입 시 1회 데미지 + 넉백 발동)
[Tooltip("대시 충돌 시 플레이어를 날리는 수평 넉백 힘 (m/s)\n" +
"0이면 넉백 없음 / 추천: 14~20 (대시에 박히는 묵직한 느낌)")]
[SerializeField] private float dashKnockbackForce = 16f; // 변수를 선언할거에요 -> 대시 수평 넉백 세기를 (크면 멀리 날아감)
[Tooltip("대시 충돌 시 플레이어를 위로 띄우는 힘 (m/s)\n" +
"0이면 수평만 / 추천: 4~8 (들이받혀서 공중에 뜨는 느낌)")]
[SerializeField] private float dashKnockbackUpForce = 6f; // 변수를 선언할거에요 -> 대시 수직 넉백 세기를 (크면 높이 뜸)
[Tooltip("대시 넉백 + 경직 지속 시간 (초)\n" +
"추천: 0.5~0.9 (대시 피해라 좀 더 길게 설정 권장)")]
[SerializeField] private float dashKnockbackDuration = 0.7f; // 변수를 선언할거에요 -> 대시 넉백 지속 시간을 (이 시간 동안 이동 불가)
[Tooltip("대시 중 플레이어 방향으로 보정하는 최대 회전 속도 (도/초)\n" +
"0이면 완전 직선 대시 (이전 방식)\n" +
"60~120이면 느리게 커브 / 180+이면 강하게 추적\n추천: 90~150")]
// [미사용] 소프트 호밍 방식 제거됨 → 매 프레임 플레이어 직접 추적 방식으로 변경
// 기존 Inspector 값이 날아가지 않도록 필드는 남겨둠
[SerializeField] private float dashHomingRate = 120f; // [미사용] 이전 소프트 호밍 회전 속도 (도/초) — 현재 사용하지 않음
// ══════════════════════════════════════════════════════════
// 대시 + 내려찍기 콤보 (Pattern_DashSmash) 전용 파라미터
// ══════════════════════════════════════════════════════════
[Header("=== 대시 + 내려찍기 콤보 (Pattern_DashSmash) ===")]
[Tooltip("콤보 대시 속도 (m/s)\n일반 대시보다 약간 빠르게 설정\n" +
"[v5 수정] 24 → 14 부자연스러운 초고속 돌진 완화\n" +
"인스펙터에서 12~16 사이로 조절 권장")]
[SerializeField] private float dashSmashSpeed = 14f; // 변수를 선언할거에요 -> 콤보 대시 속도를 (v5: 24 → 14 하향, 인스펙터에서 미세 조정)
[Tooltip("콤보 대시 최대 지속 시간 (초)\n이 시간 안에 플레이어에게 닿지 못하면 제자리에서 스매시\n추천: 0.8~1.5")]
[Range(0.3f, 3f)]
[SerializeField] private float dashSmashMaxDuration = 1.2f; // 변수를 선언할거에요 -> 콤보 대시 최대 시간을 (시간 초과 보험)
[Tooltip("콤보 대시 중 플레이어까지 이 거리 이내면 대시 중단 → 즉시 스매시 전환 (m)\n스매시 판정 반경(smashRadius)보다 약간 크게 설정 권장\n추천: smashRadius + 1")]
[SerializeField] private float dashSmashArriveRange = 4f; // 변수를 선언할거에요 -> 콤보 대시→스매시 전환 거리를
[Tooltip("콤보 전용 대시 예비 모션 시간 (초)\n0이면 일반 대시 예비 모션과 동일\n추천: 0.5~0.8 (콤보라 좀 더 알려줘야 해요)")]
[Range(0f, 2f)]
[SerializeField] private float dashSmashWindupTime = 0.6f; // 변수를 선언할거에요 -> 콤보 와인드업 시간을
[Tooltip("Phase2에서 콤보 패턴이 선택될 확률 (0~1)\n0 = 콤보 안 나옴 / 1 = 무조건 콤보\n추천: 0.3~0.5")]
[Range(0f, 1f)]
[SerializeField] private float dashSmashChance = 0.35f; // 변수를 선언할거에요 -> 콤보 선택 확률을
// ══════════════════════════════════════════════════════════
// 애니메이션 State 이름 → NorcielBoss.Animation.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 근접 패턴 테스트 버튼
// ══════════════════════════════════════════════════════════
// 근접 패턴 테스트 버튼 → BossDebugDisplay.cs로 이전됨
// ══════════════════════════════════════════════════════════
// Rigidbody 캐시 (대시 패턴 전용)
// ══════════════════════════════════════════════════════════
private Rigidbody _rb; // 변수를 선언할거에요 -> Rigidbody 캐시를 (대시 패턴에서 velocity 직접 제어)
protected override void Awake() // 함수를 실행할거에요 -> 컴포넌트 초기화를
{
base.Awake(); // 실행할거에요 -> 부모 Awake를
_rb = GetComponent<Rigidbody>(); // 캐싱할거에요 -> Rigidbody를 (대시 패턴에 사용, 없어도 작동)
}
protected override void Init() // 함수를 실행할거에요 -> 스탯/상태 초기화를
{
base.Init(); // 실행할거에요 -> 부모 Init을 (MonsterClass의 hp 등 설정)
// 보스 전용 설정
IsAggroed = true; // 설정할거에요 -> 항상 어그로 상태로
mobRenderer = null; // 비워둘거에요 -> 최적화 컬링 없음
optimizationDistance = 9999f; // 설정할거에요 -> 최적화 비활성 (항상 처리)
// ── 보스 크기 적용 ──────────────────────────────────
// 애니메이션 콜라이더가 플레이어를 밀어내거나 미끄러지게 하는 문제 완화
// Inspector에서 bossScale 조절로 실시간 테스트 가능
if (bossScale > 0f && Mathf.Abs(bossScale - 1f) > 0.01f) // 조건이 맞으면 실행할거에요 -> 스케일이 1이 아니면
{
transform.localScale = Vector3.one * bossScale; // 적용할거에요 -> 보스 크기 배율을
Debug.Log($"[Boss] 보스 크기 적용: {bossScale:F2}배 (localScale={transform.localScale})"); // 로그를 찍을거에요
}
// 상태 초기화
_state = BossState.Idle; // 설정할거에요 -> Idle로
_isBattleStarted = false; // 초기화할거에요 -> 전투 시작 플래그를
_attackTimer = 0f; // 초기화할거에요 -> 쿨타임을
_dashCooldownTimer = 0f; // 초기화할거에요 -> 대시 쿨다운을
// Phase 관리자 이벤트 구독
if (phaseManager != null) phaseManager.OnPhaseChanged += OnPhaseChanged; // 구독할거에요 -> 페이즈 전환 이벤트를
// 소환 시스템 초기화 (없으면 자동 무시)
if (summonSystem != null) summonSystem.InitializeBattle(); // 초기화할거에요 -> 소환 시스템 상태를 (재전투 시 웨이브 리셋)
// 체력
currentHP = maxHP; // 채울거에요 -> 체력을 최대로
// UI
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를 (전투 시작 전)
// 플레이어 탐색
FindPlayer(); // 실행할거에요 -> 플레이어를 씬에서 찾는
// 쇠공 초기 부착
if (ironBall != null && handHolder != null) // 조건이 맞으면 실행할거에요 -> 둘 다 Inspector에 연결됐으면
ironBall.AttachToHand(handHolder); // 붙일거에요 -> 손에
else
Debug.LogWarning("[Boss] ironBall 또는 handHolder가 Inspector에 연결되지 않았어요!"); // 경고를 찍을거에요
// 애니 대기
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
// NavMesh 안전 초기화 (오브젝트가 NavMesh 위에 올라올 때까지 대기)
StartCoroutine(InitNavMesh()); // 시작할거에요 -> NavMesh 초기화 코루틴을
}
private void FindPlayer() // 함수를 선언할거에요 -> 씬에서 플레이어를 탐색하는
{
// Tag로 먼저 탐색
GameObject p = GameObject.FindWithTag("Player"); // 찾을거에요 -> Player 태그 오브젝트를
if (p != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾았으면
{
_target = playerTransform = p.transform; // 설정할거에요 -> 타겟을
_cachedPlayerMovement = p.GetComponent<PlayerMovement>(); // 캐싱할거에요 -> PlayerMovement를 (흡인 효과용, 매번 GetComponent 방지)
return; // 종료할거에요 -> 탐색 성공
}
// 컴포넌트로 탐색 (Tag 없을 때 폴백)
PlayerMovement pm = FindObjectOfType<PlayerMovement>(); // 찾을거에요 -> PlayerMovement 컴포넌트로
if (pm != null) // 조건이 맞으면 실행할거에요 -> 컴포넌트로 찾았으면
{
_target = playerTransform = pm.transform; // 설정할거에요 -> 타겟을
_cachedPlayerMovement = pm; // 캐싱할거에요 -> PlayerMovement를 (흡인 효과용)
return; // 종료할거에요 -> 탐색 성공
}
Debug.LogError("[Boss] 플레이어를 찾지 못했어요! 플레이어에 'Player' 태그를 설정해주세요."); // 에러를 찍을거에요
}
private IEnumerator InitNavMesh() // 코루틴을 정의할거에요 -> NavMesh 안전 초기화를
{
yield return null; // 기다릴거에요 -> 1프레임 (씬 로딩 완료 대기)
if (agent == null) yield break; // 중단할거에요 -> 에이전트 없으면
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh 위에 올라올 때까지
{
retry++; // 증가할거에요 -> 재시도 횟수를
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초를
}
if (!agent.isOnNavMesh) Debug.LogError("[Boss] NavMesh 초기화 실패! 보스가 NavMesh 위에 있는지 확인해주세요."); // 에러를 찍을거에요
agent.isStopped = true; // 멈출거에요 -> 전투 시작 전까지 정지
agent.enabled = false; // 끌거에요 -> 전투 시작 전까지 비활성 (BattleStart에서 켜짐)
}
private void Start() // 함수를 실행할거에요 -> 씬 시작 시
{
// ⚠️ 보스전은 트리거존(BossZoneTrigger)에서 StartBossBattle()을 호출해야 시작돼요
// 트리거존 없이 바로 시작하려면 아래 주석을 풀어주세요
// StartBossBattle();
// ── 플레이어 행동 추적기에 보스 자동 등록 ──
// 보스가 다른 씬에 있어도 런타임에 안전하게 연결됨
if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 추적기가 존재하면
PlayerBehaviorTracker.Instance.RegisterBoss(transform, counterSystem); // 등록할거에요 -> 보스 자신과 카운터 시스템을
}
private void OnDestroy() // 함수를 실행할거에요 -> 보스가 파괴될 때
{
// Phase 관리자 이벤트 구독 해제 (메모리 누수 방지)
if (phaseManager != null) phaseManager.OnPhaseChanged -= OnPhaseChanged; // 해제할거에요 -> 페이즈 전환 이벤트를
}
// ══════════════════════════════════════════════════════════
// Update
// ══════════════════════════════════════════════════════════
protected override void Update() // 함수를 실행할거에요 -> 매 프레임을
{
base.Update(); // 실행할거에요 -> 부모 Update를
if (isDead) return; // 중단할거에요 -> 사망 상태면
// Phase 체크 → phaseManager에 위임
if (phaseManager != null && _isBattleStarted) // 조건이 맞으면 실행할거에요 -> 전투 중이면
phaseManager.CheckPhases(currentHP, maxHP); // 체크할거에요 -> 현재 HP 기반 페이즈 전환을 (MonsterClass의 currentHP, maxHP 사용)
// [D-3 수정] 플레이어 캐시 주기적 갱신 — 리스폰으로 오브젝트가 교체될 경우 재탐색
if (_isBattleStarted)
{
_targetRefreshTimer -= Time.deltaTime; // 줄일거에요 -> 갱신 타이머를
if (_targetRefreshTimer <= 0f) // 조건이 맞으면 실행할거에요 -> 갱신 시점이면
{
_targetRefreshTimer = 3f; // 리셋할거에요 -> 3초 간격으로
if (_target == null || !_target.gameObject.activeInHierarchy) // 조건이 맞으면 실행할거에요 -> 타겟이 없거나 비활성화됐으면
{
Debug.Log("[Boss] 플레이어 캐시 만료 → 재탐색"); // 로그를 찍을거에요
FindPlayer(); // 재탐색할거에요 -> 씬에서 플레이어를
}
}
}
// ── 대시 쿨다운 감소 (매 프레임) ──
if (_dashCooldownTimer > 0f) // 조건이 맞으면 실행할거에요 -> 대시 쿨다운 남아있으면
_dashCooldownTimer -= Time.deltaTime; // 줄일거에요 -> 쿨다운 타이머를
TrackPlayerBehavior(); // 추적할거에요 -> 플레이어의 이동/정지/접근/도주 행동을
RunFSM(); // 실행할거에요 -> FSM 메인 로직을
}
// ══════════════════════════════════════════════════════════
// 플레이어 행동 추적 (매 프레임)
// → NorcielBoss.Tracking.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// Phase 전환 — BossPhaseManager에서 이벤트 수신
// ══════════════════════════════════════════════════════════
/// <summary>PhaseManager에서 페이즈 전환 이벤트 수신</summary>
private void OnPhaseChanged(int phaseNumber) // 함수를 선언할거에요 -> 페이즈 변경 이벤트를 처리하는
{
// [D-4 수정] 멀티히트 공격 등으로 같은 프레임에 여러 번 호출될 때 중복 루틴 방지
if (_phaseTransitioning) return; // 중단할거에요 -> 이미 전환 중이면 (중복 발동 차단)
_phaseTransitioning = true; // 설정할거에요 -> 전환 시작 플래그를
if (phaseNumber == 2) StartCoroutine(PhaseTransitionRoutine(2)); // 시작할거에요 -> Phase2 전환 연출을
else if (phaseNumber == 3) StartCoroutine(PhaseTransitionRoutine(3)); // 시작할거에요 -> Phase3 전환 연출을
}
private IEnumerator PhaseTransitionRoutine(int phase) // 코루틴을 정의할거에요 -> 페이즈 전환 연출을 처리하는
{
// 현재 패턴 실행 중이면 끝날 때까지 기다림
// [C-4 수정] 타임아웃 추가 → Attacking/Recovering이 영구 고착 시 무한루프 방지
float phaseWaitTimeout = 10f; // 설정할거에요 -> 최대 대기 시간을 (10초)
float phaseWaitElapsed = 0f; // 초기화할거에요 -> 대기 경과 시간을
while ((_state == BossState.Attacking || _state == BossState.Recovering)
&& phaseWaitElapsed < phaseWaitTimeout) // 반복할거에요 -> 패턴 실행 중이면 (타임아웃 포함)
{
phaseWaitElapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
yield return null; // 기다릴거에요 -> 1프레임을
}
if (phaseWaitElapsed >= phaseWaitTimeout) // 조건이 맞으면 실행할거에요 -> 타임아웃이면
Debug.LogWarning($"[Boss] PhaseTransition 대기 타임아웃 ({phaseWaitTimeout}s) → 강제 진행"); // 경고를 찍을거에요
SetState(BossState.Attacking); // 잠글거에요 -> 다른 행동 차단
// ══════════════════════════════════════════════════════════
// [v4 추가] Phase 전환 구조 변화 — 원칙 8, 10 적용
//
// 포효만으로는 "규칙이 바뀌었다"를 체감할 수 없음.
// 전환 시 넉백 + 카메라 셰이크 + 시각 피드백 추가.
// 플레이어는 전환 중에도 움직일 수 있음 (원칙 10).
// ══════════════════════════════════════════════════════════
// ── Phase3 전환: 보스가 웅크렸다 폭발적으로 일어남 → 주변 넉백 ──
if (phase == 3) // 조건이 맞으면 실행할거에요 -> Phase3 전환이면
{
// 웅크리기 연출 (짧은 정지, 포효 전 텔)
PlayIdleAnim(); // 재생할거에요 -> 대기 애니를 (웅크리는 연출 대용)
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 웅크리기 시간을 (0.5초)
}
PlayBossSFX(sfx_Roar, sfx_Roar_Vol); // 재생할거에요 -> 포효 사운드를 (Phase 전환 연출)
PlayBossVFX(vfx_Roar); // 재생할거에요 -> 포효 이펙트를 (보스 주변 폭발 파티클)
yield return StartCoroutine(PlayAnimAndWait(anim_Roar, 5f)); // 기다릴거에요 -> 포효 종료를 (최대 5초, 10초에서 단축)
// ── 전환 완료 시 주변 넉백 (Phase2: 약하게, Phase3: 강하게) ──
// [원칙 8] "규칙이 바뀌었다"를 물리적으로 체감시킴
if (_target != null && _cachedPlayerMovement != null) // 조건이 맞으면 실행할거에요 -> 플레이어 있으면
{
float pushForce = (phase == 3) ? 12f : 6f; // 설정할거에요 -> 넉백 세기를 (Phase3이 2배 강함)
float pushUp = (phase == 3) ? 4f : 2f; // 설정할거에요 -> 수직 넉백을
float pushDuration = (phase == 3) ? 0.6f : 0.3f; // 설정할거에요 -> 넉백 지속을
Vector3 pushDir = (_target.position - transform.position); // 계산할거에요 -> 보스→플레이어 방향을
pushDir.y = 0f; // 제거할거에요 -> 수직 성분을
float distToPlayer = pushDir.magnitude; // 계산할거에요 -> 거리를
if (distToPlayer < 15f) // 조건이 맞으면 실행할거에요 -> 15m 이내면 넉백 적용
{
_cachedPlayerMovement.ApplyKnockback(pushDir, pushForce, pushUp, pushDuration); // 밀어낼거에요 -> 플레이어를
Debug.Log($"[Boss] Phase{phase} 전환 넉백! force={pushForce} dist={distToPlayer:F1}m"); // 로그를 찍을거에요
}
}
if (phase == 2 && phaseManager != null) // 조건이 맞으면 실행할거에요 -> Phase2이면
{
phaseManager.ConfirmPhase2(); // 확정할거에요 -> Phase2를
float intervalMult = phaseManager.GetIntervalMult(); // 가져올거에요 -> 간격 배율을
float speedMult = phaseManager.GetSpeedMult(); // 가져올거에요 -> 속도 배율을
patternInterval *= intervalMult; // 곱할거에요 -> 패턴 간격에 배율을
if (agent != null) agent.speed *= speedMult; // 곱할거에요 -> 이동 속도에 배율을
Debug.Log($"[Boss] Phase2 진입! interval×{intervalMult:F2}, speed×{speedMult:F2} [패턴 변형: Smash+충격파, Sweep 왕복]"); // 로그를 찍을거에요
}
else if (phase == 3 && phaseManager != null) // 조건이 맞으면 실행할거에요 -> Phase3이면
{
// [C-3 수정] ConfirmPhase3() 이전에 현재(Phase2) 배율을 먼저 저장
// ConfirmPhase3() 이후 GetIntervalMult()는 Phase3 값 반환
// → Phase3Mult / Phase3Mult = 1.0 이 되어 배율이 무효화되는 버그 방지
float currentIntervalMult = phaseManager.IsPhase2 ? phaseManager.GetIntervalMult() : 1f; // 저장할거에요 -> Phase3 확정 전 현재 간격 배율을
float currentSpeedMult = phaseManager.IsPhase2 ? phaseManager.GetSpeedMult() : 1f; // 저장할거에요 -> Phase3 확정 전 현재 속도 배율을
phaseManager.ConfirmPhase3(); // 확정할거에요 -> Phase3를
float phase3IntervalMult = phaseManager.GetIntervalMult(); // 가져올거에요 -> Phase3 간격 배율을
float phase3SpeedMult = phaseManager.GetSpeedMult(); // 가져올거에요 -> Phase3 속도 배율을
// Phase2 위에 Phase3 증분만 추가 적용
patternInterval *= (phase3IntervalMult / currentIntervalMult); // 적용할거에요 -> Phase3 증분 간격 배율을
if (agent != null) agent.speed *= (phase3SpeedMult / currentSpeedMult); // 적용할거에요 -> Phase3 증분 속도 배율을
Debug.Log($"[Boss] Phase3 (광폭화) 진입! interval×{phase3IntervalMult / currentIntervalMult:F2}, speed×{phase3SpeedMult / currentSpeedMult:F2} [패턴 변형: Smash 3연타, Sweep 360°]"); // 로그를 찍을거에요
}
_phaseTransitioning = false; // 해제할거에요 -> 전환 완료 플래그를 (D-4: 다음 Phase 전환 허용)
SetState(BossState.PreAttack); // 전환할거에요 -> 공격 대기로
}
// ══════════════════════════════════════════════════════════
// FSM 메인 로직
// → NorcielBoss.FSM.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 패턴 선택 (SelectAndFire, StartPattern)
// → NorcielBoss.PatternSelection.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 패턴 구현들 (Pattern_Throw, Pattern_Smash, Pattern_Sweep, Pattern_Dash, Pattern_DashSmash)
// 및 쇠공 줍기 (PickupBall), 휴식 (BossRest)
// → NorcielBoss.Patterns.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// TakeDamage 오버라이드 — 보스 전용 슈퍼아머 피격 처리
//
// [부모(MonsterClass)의 TakeDamage 문제]
// 부모는 데미지 적용 후 StartHit()을 호출해요.
// StartHit()은 isHit=true, agent.isStopped=true, 피격 애니 재생을 해요.
// → 보스 FSM이 관리하는 이동/대시/패턴을 강제로 끊어버려요.
//
// [보스 전용 처리]
// StartHit() 호출 없이 HP만 깎고 시각 피드백(flash)만 줘요.
// → 보스는 피격에 행동이 안 끊기는 슈퍼아머 상태예요.
// → Phase2 체크는 매 프레임 CheckPhase2()가 담당하므로 여기서 불필요.
// ──────────────────────────────────────────────────────────
public override void TakeDamage(float amount) // 함수를 정의할거에요 -> 보스 피격 처리를 (슈퍼아머 버전)
{
if (isDead || _state == BossState.Dead) return; // 중단할거에요 -> 이미 죽었으면
// ApplyDamageOnly: MonsterClass의 protected 메서드
// 내부에서 IsAggroed=true, currentHP -= amount, OnHealthChanged.Invoke 를 모두 처리해요.
// OnHealthChanged 이벤트는 선언 클래스(MonsterClass) 내부에서만 직접 호출 가능하므로
// 자식 클래스에서는 이 메서드를 통해 우회해요.
ApplyDamageOnly(amount); // 깎을거에요 -> 체력을 + UI 갱신을 (MonsterClass protected 메서드)
_recentHitCount++; // 증가시킬거에요 -> 최근 피격 카운터를 (카이팅 감지에 사용)
_recentHitDecay = 0f; // 초기화할거에요 -> 감쇠 타이머를 (맞을 때마다 리셋)
if (visualFX != null) visualFX.Flash(); // 재생할거에요 -> 피격 플래시 효과를 (FSM 방해 없이 시각 피드백만)
PlayBossSFX(sfx_Hit, sfx_Hit_Vol); // 재생할거에요 -> 피격 사운드를 (슈퍼아머 피격음)
PlayBossVFX(vfx_Hit); // 재생할거에요 -> 피격 이펙트를 (피 튀김 등)
Debug.Log($"[Boss] 💥 피격! 데미지={amount:F0} HP={currentHP:F0}/{maxHP:F0}"); // 로그를 찍을거에요
// ── [소환 시스템] HP 구간별 잡몹 소환 체크 ──────────
if (summonSystem != null) // 조건이 맞으면 실행할거에요 -> 소환 시스템 있으면
{
float hpRatio = (maxHP > 0f) ? (currentHP / maxHP) : 1f; // 계산할거에요 -> 현재 HP 비율을
Debug.Log($"[BossSummon] CheckSummon 호출! HP비율={hpRatio * 100f:F1}% currentHP={currentHP:F0} maxHP={maxHP:F0}"); // 로그를 찍을거에요 -> 소환 체크 진입 확인용
summonSystem.CheckSummon(currentHP, maxHP); // 체크할거에요 -> HP 비율로 소환 발동 여부를
}
else // 소환 시스템 없으면
{
Debug.LogWarning("[BossSummon] summonSystem이 NULL! Inspector에서 연결 확인 필요"); // 경고를 찍을거에요
}
if (currentHP <= 0f) // 조건이 맞으면 실행할거에요 -> HP가 바닥났으면
{
Die(); // 실행할거에요 -> 사망 처리를 (MonsterClass.Die → OnDie 순서)
}
// ✅ StartHit() 호출 안 함 → agent 정지·피격 애니·isHit 플래그 없음 → FSM 완전 보존
}
// ══════════════════════════════════════════════════════════
// Die 오버라이드 — 사망 애니메이션 완료 후 Destroy 처리
// 부모 Die()는 1.5초 후 SetActive(false) → 보스는 애니 끝나면 완전 파괴
// ══════════════════════════════════════════════════════════
protected override void Die() // 오버라이드할거에요 -> 보스 전용 사망 처리를
{
base.Die(); // 실행할거에요 -> 부모 Die()를 (isDead=true, 애니 재생, ReturnToPool/페이드 예약 등)
CancelInvoke(nameof(ReturnToPool)); // 취소할거에요 -> 부모가 예약한 1.5초 후 SetActive(false)를
// ── 플레이어 행동 추적기에서 보스 등록 해제 ──
if (PlayerBehaviorTracker.Instance != null) // 조건이 맞으면 실행할거에요 -> 추적기가 존재하면
PlayerBehaviorTracker.Instance.UnregisterBoss(); // 해제할거에요 -> 보스 참조를
// ── 소환된 잡몹 정리 (보스 사망 시 잡몹도 같이 사라지게) ──
// 선택사항: 보스 죽어도 잡몹은 남기고 싶으면 이 블록 삭제
if (summonSystem != null) summonSystem.InitializeBattle(); // 정리할거에요 -> 남아있는 소환 몬스터를
// ※ visualFX의 DeathFadeRoutine은 그대로 둠
// → 페이드 연출은 살리되, SetActive(false)보다 Destroy가 먼저 처리됨
// → Destroy 시점에 페이드가 아직 진행 중이면 자연스럽게 중단됨
StartCoroutine(DeathSequence()); // 시작할거에요 -> 사망 연출 코루틴을
}
/// <summary>
/// 사망 애니메이션이 끝날 때까지 기다린 뒤 게임 오브젝트를 Destroy
/// </summary>
private IEnumerator DeathSequence() // 코루틴을 정의할거에요 -> 사망 연출 시퀀스를
{
// 1프레임 대기 → animator.Play(Monster_Die)가 반영되기를 기다림
yield return null; // 대기할거에요 -> 1프레임을 (애니메이터 상태 갱신용)
float deathAnimLength = 2f; // 기본값을 설정할거에요 -> 애니메이션 길이를 (폴백용 2초)
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 유효하면
{
AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 재생 중인 애니 정보를
deathAnimLength = stateInfo.length; // 읽을거에요 -> 애니메이션 총 길이를 (초 단위)
// 안전장치: 너무 짧거나 비정상이면 최소 1초 보장
if (deathAnimLength < 0.5f) // 조건이 맞으면 실행할거에요 -> 비정상적으로 짧으면
deathAnimLength = 2f; // 보정할거에요 -> 기본 2초로
}
PlayBossSFX(sfx_Death, sfx_Death_Vol); // 재생할거에요 -> 사망 사운드를 (보스 최후 사운드)
Debug.Log($"[Boss] 사망 애니 재생 중... {deathAnimLength:F1}초 후 Destroy"); // 로그를 찍을거에요
yield return new WaitForSeconds(deathAnimLength); // 대기할거에요 -> 사망 애니가 끝날 때까지
Debug.Log("[Boss] 사망 애니 완료 → Destroy"); // 로그를 찍을거에요
Destroy(gameObject); // 파괴할거에요 -> 보스 게임 오브젝트를 완전히
}
protected override void OnDie() // 함수를 정의할거에요 -> 사망 처리를
{
SetState(BossState.Dead); // 설정할거에요 -> 사망 상태로
StopAgent(); // 멈출거에요 -> NavMesh를
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
// ── 보스방 벽 해제 (트리거존 연결 시) ───────────────
if (zoneTrigger != null) zoneTrigger.OpenWalls(); // 열거에요 -> 보스방 벽을 (플레이어 탈출 가능)
if (ObsessionSystem.instance != null)
ObsessionSystem.instance.AddRunXP(100); // 지급할거에요 -> 경험치를
else
Debug.LogError("[Boss] ObsessionSystem이 없어요!"); // 에러를 찍을거에요
base.OnDie(); // 실행할거에요 -> 부모 사망 처리를
}
// ══════════════════════════════════════════════════════════
// FSM 상태 변경
// → NorcielBoss.FSM.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// NavMesh/Movement 유틸
// → NorcielBoss.Movement.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 애니메이션 유틸
// → NorcielBoss.Animation.cs로 이전됨
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// 테스트 버튼 폴링
// → BossDebugDisplay.cs로 이전됨 (debugDisplay 참조)
// ══════════════════════════════════════════════════════════
// ══════════════════════════════════════════════════════════
// ExecuteAILogic 오버라이드 (부모 AI 비활성화)
// ══════════════════════════════════════════════════════════
protected override void ExecuteAILogic() { } // 비워둘거에요 -> 보스 AI는 RunFSM이 처리하므로 부모 로직 차단
// ══════════════════════════════════════════════════════════
// 씬 뷰 기즈모 / 게임 화면 디버그 로그
// → BossDebugDisplay.cs로 이전됨 (debugDisplay 참조)
// ══════════════════════════════════════════════════════════
}