Projext/Assets/02_Scripts/Enemy/BossAI/NorcielBoss.cs
hydrozen e989d20668 카툰 쉐이더 추가 + 중복 스크립트 수정 + 전체 업데이트
- ToonPostProcess.shader: 횃불 고딕 스타일 후처리 쉐이더 (Built-in RP)
- ToonCameraEffect.cs: 카메라 자동 부착 후처리 스크립트
- 중복 UI 스크립트 제거 (MenuIntroController, ToggleCustom)
- 씬, 프리팹, 애니메이션 등 전체 업데이트

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 12:31:16 +09:00

805 lines
60 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 참조)
// ══════════════════════════════════════════════════════════
}