1177 lines
75 KiB
C#
1177 lines
75 KiB
C#
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
|
||
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
|
||
|
||
// ============================================================
|
||
// NorcielBoss — 버그 수정 + 고도화 버전
|
||
//
|
||
// ✅ 수정된 버그:
|
||
// ① 공이 안 날아감 → SetParent 후 월드 좌표 명시 복원 + 포물선 물리
|
||
// ② 대시가 안 됨 → OnDashStart 이벤트 의존 제거 → 타이머 방식
|
||
// ③ 데미지 없음 → OverlapSphere 높이 보정 + GetComponentInParent 추가
|
||
//
|
||
// ✅ 고도화:
|
||
// - 페이즈 시스템 HP 50% 이하 → Phase2 (속도↑ 간격↓ 데미지↑)
|
||
// - 연속 스매쉬 Phase2 전용: 찍기 2회 연속
|
||
// - 돌진+찍기 콤보 Phase2 전용: 돌진 후 즉시 찍기 연계
|
||
//
|
||
// 애니메이션 이벤트 등록 목록:
|
||
// Attack_Throw → 공 손 떠나는 프레임 : OnThrowRelease
|
||
// Attack_Throw → 마지막 프레임 : OnAnimEnd
|
||
// Skill_Smash_Impact → 쇠공 바닥 닿는 프레임 : OnSmashHit
|
||
// Skill_Smash_Impact → 마지막 프레임 : OnAnimEnd
|
||
// Skill_Sweep → 쓸리는 프레임 : OnSweepHit
|
||
// Skill_Sweep → 마지막 프레임 : OnAnimEnd
|
||
// Skill_Pickup → 마지막 프레임 : OnAnimEnd
|
||
// Roar → 마지막 프레임 : OnAnimEnd
|
||
// ※ Skill_Dash_Ready → OnDashStart 이벤트 더 이상 불필요
|
||
// ============================================================
|
||
|
||
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
|
||
{
|
||
// ──────────────────────────────────────────────────────────
|
||
// 패턴 열거형
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private enum BossPattern // 열거형을 선언할거에요 -> 보스 공격 패턴 종류를
|
||
{
|
||
Throw, // 쇠공 던지기
|
||
Smash, // 내려찍기
|
||
Sweep, // 휩쓸기
|
||
Dash, // 돌진
|
||
ComboSmash, // 연속 스매쉬 2회 (Phase2 전용)
|
||
DashSmash, // 돌진 + 찍기 콤보 (Phase2 전용)
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// Inspector — 두뇌 / 기본
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
[Header("=== 두뇌 연결 ===")]
|
||
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 패턴 결정 카운터 시스템을
|
||
|
||
[Header("=== 기본 패턴 설정 ===")]
|
||
[Tooltip("공격과 다음 공격 사이 대기 시간 (초)")]
|
||
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 간격을
|
||
[Tooltip("이 거리 이하 = 근접 범위 (찍기/휩쓸기 우선)")]
|
||
[SerializeField] private float meleeRange = 3.5f; // 변수를 선언할거에요 -> 근접 공격 사거리를
|
||
[Tooltip("이 거리 이상 = 원거리 범위 (던지기 우선)")]
|
||
[SerializeField] private float throwRange = 8f; // 변수를 선언할거에요 -> 원거리 던지기 사거리를
|
||
[Tooltip("이 거리 이상 지속 = 보스가 대시로 추격 (도망 감지)")]
|
||
[SerializeField] private float dashChaseRange = 10f; // 변수를 선언할거에요 -> 대시 추격 발동 거리를
|
||
[Tooltip("대시 추격 발동까지 플레이어가 멀리 있어야 하는 시간 (초)")]
|
||
[SerializeField] private float dashChaseDelay = 2f; // 변수를 선언할거에요 -> 도망 감지 누적 시간을
|
||
[Tooltip("Phase2 진입 HP 비율 (0.5 = 50%)")]
|
||
[SerializeField][Range(0f, 1f)] private float phase2HpRatio = 0.5f; // 변수를 선언할거에요 -> 페이즈2 전환 체력 비율을
|
||
|
||
[Header("=== 쇠공 설정 ===")]
|
||
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던질 쇠공 오브젝트를
|
||
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 쇠공 쥐는 손 위치를
|
||
[Tooltip("handHolder 기준 쇠공 위치 오프셋 (손 본 위치가 맞지 않을 때 조정)")]
|
||
[SerializeField] private Vector3 ballHoldOffset = Vector3.zero; // 변수를 선언할거에요 -> 쇠공 부착 위치 오프셋을
|
||
[Tooltip("포물선 최고 높이 (클수록 더 높이 올라감)")]
|
||
[SerializeField] private float throwArcHeight = 4f; // 변수를 선언할거에요 -> 포물선 높이를
|
||
[Tooltip("쇠공 착지 범위 판정 반경")]
|
||
[SerializeField] private float ballLandRadius = 2f; // 변수를 선언할거에요 -> 착지 판정 반경을
|
||
[Tooltip("쇠공 착지 데미지")]
|
||
[SerializeField] private float ballLandDamage = 20f; // 변수를 선언할거에요 -> 착지 데미지를
|
||
|
||
[Header("=== 찍기 설정 ===")]
|
||
[SerializeField] private float smashRadius = 2.5f; // 변수를 선언할거에요 -> 찍기 판정 반경을
|
||
[SerializeField] private float smashDamage = 25f; // 변수를 선언할거에요 -> 찍기 데미지를
|
||
[Tooltip("OverlapSphere 중심 높이 보정 (발 기준 → 허리 높이로)")]
|
||
[SerializeField] private float smashHeightOffset = 0.5f; // 변수를 선언할거에요 -> 찍기 판정 높이 오프셋을 ← 버그③ 수정
|
||
|
||
[Header("=== 휩쓸기 설정 ===")]
|
||
[SerializeField] private float sweepRadius = 3.5f; // 변수를 선언할거에요 -> 휩쓸기 판정 반경을
|
||
[SerializeField] private float sweepDamage = 18f; // 변수를 선언할거에요 -> 휩쓸기 데미지를
|
||
[SerializeField] private float sweepHeightOffset = 0.5f; // 변수를 선언할거에요 -> 휩쓸기 판정 높이 오프셋을
|
||
|
||
[Header("=== 돌진 설정 ===")]
|
||
[Tooltip("돌진 준비 자세 유지 시간 (초) — 이벤트 대신 타이머 사용")]
|
||
[SerializeField] private float dashReadyDuration = 0.8f; // 변수를 선언할거에요 -> 돌진 준비 시간을 ← 버그② 핵심
|
||
[SerializeField] private float dashSpeed = 18f; // 변수를 선언할거에요 -> 돌진 속도를
|
||
[SerializeField] private float dashDuration = 0.5f; // 변수를 선언할거에요 -> 돌진 지속 시간을
|
||
[SerializeField] private float dashDamage = 30f; // 변수를 선언할거에요 -> 돌진 충돌 데미지를
|
||
|
||
[Header("=== Phase2 강화 수치 ===")]
|
||
[Tooltip("Phase2 공격 간격 배율 (0.65 = 35% 단축)")]
|
||
[SerializeField] private float phase2IntervalMult = 0.65f; // 변수를 선언할거에요 -> 페이즈2 공격 간격 배율을
|
||
[Tooltip("Phase2 데미지 배율")]
|
||
[SerializeField] private float phase2DamageMult = 1.4f; // 변수를 선언할거에요 -> 페이즈2 데미지 배율을
|
||
[Tooltip("Phase2 이동 속도 배율")]
|
||
[SerializeField] private float phase2SpeedMult = 1.3f; // 변수를 선언할거에요 -> 페이즈2 이동 속도 배율을
|
||
|
||
[Header("=== UI ===")]
|
||
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스 체력바 UI를
|
||
|
||
[Header("=== 애니메이션 State 이름 ===")]
|
||
[SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니 이름을
|
||
[SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니 이름을
|
||
[SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니 이름을
|
||
[SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니 이름을
|
||
[SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 줍기 애니 이름을
|
||
[SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니 이름을
|
||
[SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 실행 애니 이름을
|
||
[SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 찍기 애니 이름을
|
||
[SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니 이름을
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 내부 상태 변수
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private Rigidbody _rb; // 변수를 선언할거에요 -> 보스 Rigidbody를
|
||
private Transform _target; // 변수를 선언할거에요 -> 공격 대상 Transform을
|
||
|
||
private bool _isBattleStarted = false; // 변수를 선언할거에요 -> 전투 시작 여부를
|
||
private bool _isWeaponless = false; // 변수를 선언할거에요 -> 쇠공 없는 상태 여부를
|
||
private bool _isPerformingAction = false; // 변수를 선언할거에요 -> 패턴 실행 중 여부를
|
||
private bool _isDashing = false; // 변수를 선언할거에요 -> 돌진 중 여부를 (충돌 판정용)
|
||
private bool _isPhase2 = false; // 변수를 선언할거에요 -> 페이즈2 여부를
|
||
private bool _phase2Triggered = false; // 변수를 선언할거에요 -> 페이즈2 이미 발동됐는지를
|
||
|
||
private float _attackTimer = 0f; // 변수를 선언할거에요 -> 공격 쿨타임 타이머를
|
||
private float _farFromBossTimer = 0f; // 변수를 선언할거에요 -> 플레이어가 멀리 있던 누적 시간을 (도망 감지용)
|
||
|
||
// 애니메이션 이벤트 수신 플래그
|
||
private bool _animEndFired = false; // 변수를 선언할거에요 -> OnAnimEnd 수신 여부를
|
||
private bool _throwFired = false; // 변수를 선언할거에요 -> OnThrowRelease 수신 여부를
|
||
private bool _smashHitFired = false; // 변수를 선언할거에요 -> OnSmashHit 수신 여부를
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 초기화
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
protected override void Awake() // 함수를 실행할거에요 -> 컴포넌트 초기화를
|
||
{
|
||
base.Awake(); // 실행할거에요 -> 부모 Awake를
|
||
_rb = GetComponent<Rigidbody>(); // 가져올거에요 -> 보스 Rigidbody를
|
||
}
|
||
|
||
protected override void Init() // 함수를 실행할거에요 -> 스탯·상태 초기화를
|
||
{
|
||
base.Init(); // 실행할거에요 -> 부모 Init을
|
||
|
||
_attackTimer = 0f; // 초기화할거에요 -> 타이머를
|
||
_isBattleStarted = _isWeaponless = _isPerformingAction = _isDashing = false; // 초기화할거에요 -> 모든 상태를
|
||
_isPhase2 = _phase2Triggered = false; // 초기화할거에요 -> 페이즈 상태를
|
||
|
||
IsAggroed = true; // 설정할거에요 -> 항상 어그로 상태로
|
||
mobRenderer = null; // 초기화할거에요 -> 보스는 최적화 안 함
|
||
optimizationDistance = 9999f; // 설정할거에요 -> 최적화 거리를 무한대로
|
||
|
||
// 플레이어 탐색
|
||
GameObject playerObj = GameObject.FindWithTag("Player"); // 찾을거에요 -> Player 태그로
|
||
if (playerObj != null)
|
||
_target = playerTransform = playerObj.transform; // 설정할거에요 -> 타겟을
|
||
else
|
||
{
|
||
PlayerMovement pm = FindObjectOfType<PlayerMovement>(); // 찾을거에요 -> 컴포넌트로
|
||
if (pm != null) _target = playerTransform = pm.transform; // 설정할거에요 -> 타겟을
|
||
}
|
||
if (_target == null) Debug.LogError("[Boss] 플레이어를 찾지 못했어요!"); // 에러를 찍을거에요
|
||
|
||
currentHP = maxHP; // 채울거에요 -> 체력을 최대로
|
||
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
|
||
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
||
|
||
// ── 쇠공 초기 셋업 ──────────────────────────────────
|
||
// 씬에 별도 배치된 오브젝트든, handHolder 자식이든 상관없이
|
||
// 시작 시 handHolder에 붙이고 Rigidbody를 고정함
|
||
if (ironBall != null) // 조건이 맞으면 실행할거에요 -> 쇠공 있으면
|
||
{
|
||
Rigidbody initRb = ironBall.GetComponent<Rigidbody>(); // 가져올거에요 -> Rigidbody를
|
||
if (initRb != null) // 조건이 맞으면 실행할거에요 -> Rigidbody 있으면
|
||
{
|
||
initRb.isKinematic = true; // 고정할거에요 -> 물리를 (손에 들고 있는 상태)
|
||
initRb.velocity = Vector3.zero; // 초기화할거에요 -> 속도를
|
||
initRb.angularVelocity = Vector3.zero; // 초기화할거에요 -> 회전 속도를
|
||
}
|
||
|
||
// 보스 자신의 콜라이더와 쇠공 콜라이더 충돌 무시 (밀려남 방지)
|
||
Collider ballCol = ironBall.GetComponent<Collider>(); // 가져올거에요 -> 쇠공 콜라이더를
|
||
Collider bossCol = GetComponent<Collider>(); // 가져올거에요 -> 보스 콜라이더를
|
||
if (ballCol != null && bossCol != null) // 조건이 맞으면 실행할거에요 -> 둘 다 있으면
|
||
{
|
||
Physics.IgnoreCollision(ballCol, bossCol, true); // 무시할거에요 -> 보스↔쇠공 충돌을
|
||
Debug.Log("[Boss] 쇠공↔보스 콜라이더 충돌 무시 설정 완료"); // 로그를 찍을거에요
|
||
}
|
||
|
||
if (handHolder != null) // 조건이 맞으면 실행할거에요 -> 손 위치가 지정돼있으면
|
||
{
|
||
AttachBallToHand(); // 실행할거에요 -> 손에 부착을
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> handHolder 없으면
|
||
{
|
||
Debug.LogWarning("[Boss] handHolder 슬롯이 비어있어요! Inspector에서 손 본(Bone) Transform 연결 필요"); // 경고를 찍을거에요
|
||
}
|
||
}
|
||
_isWeaponless = false; // 초기화할거에요 -> 무기 있는 상태로
|
||
|
||
StartCoroutine(SafeInitNavMesh()); // 시작할거에요 -> NavMesh 안전 초기화를
|
||
}
|
||
|
||
private IEnumerator SafeInitNavMesh() // 코루틴을 정의할거에요 -> NavMesh 초기화를
|
||
{
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
if (agent == null) yield break; // 중단할거에요 -> 에이전트 없으면
|
||
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
|
||
while (!agent.isOnNavMesh && retry < 5) { retry++; yield return new WaitForSeconds(0.1f); } // 기다릴거에요 -> NavMesh 위에 올 때까지
|
||
if (agent.isOnNavMesh) agent.isStopped = true; // 멈출거에요 -> 올라오면
|
||
agent.enabled = false; // 끌거에요 -> 전투 시작 전까지
|
||
}
|
||
|
||
private void Start() // 함수를 실행할거에요 -> 시작 시 전투 개시를
|
||
{
|
||
StartBossBattle(); // 실행할거에요 -> 보스전 시작을
|
||
}
|
||
|
||
protected override void Update() // 함수를 실행할거에요 -> 매 프레임을
|
||
{
|
||
base.Update(); // 실행할거에요 -> 부모 Update를
|
||
if (!isDead)
|
||
{
|
||
CheckPhase2(); // 확인할거에요 -> 페이즈2 전환 조건을
|
||
ExecuteAILogic(); // 실행할거에요 -> AI 로직을
|
||
PollTestButtons(); // 확인할거에요 -> Inspector 테스트 버튼 입력을
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 페이즈2 체크
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void CheckPhase2() // 함수를 선언할거에요 -> HP 비율로 페이즈2 전환을 감지하는
|
||
{
|
||
if (_phase2Triggered || maxHP <= 0) return; // 중단할거에요 -> 이미 전환됐거나 maxHP 0이면
|
||
|
||
if (currentHP / maxHP <= phase2HpRatio) // 조건이 맞으면 실행할거에요 -> HP가 50% 이하가 됐으면
|
||
{
|
||
_phase2Triggered = true; // 설정할거에요 -> 이미 발동됨으로
|
||
StartCoroutine(EnterPhase2()); // 시작할거에요 -> 페이즈2 진입 코루틴을
|
||
}
|
||
}
|
||
|
||
private IEnumerator EnterPhase2() // 코루틴을 정의할거에요 -> 페이즈2 진입 연출을
|
||
{
|
||
while (_isPerformingAction) yield return null; // 기다릴거에요 -> 현재 패턴 끝날 때까지
|
||
|
||
_isPerformingAction = true; // 잠글거에요 -> 추격/공격 차단
|
||
|
||
Debug.Log("[Boss] ⚡ Phase2 진입!"); // 로그를 찍을거에요
|
||
|
||
yield return StartCoroutine(PlayAndWait(anim_Roar, 8f)); // 기다릴거에요 -> 분노 포효 애니 종료를
|
||
|
||
_isPhase2 = true; // 설정할거에요 -> 페이즈2 상태로
|
||
|
||
if (agent != null) agent.speed *= phase2SpeedMult; // 높일거에요 -> 이동 속도를
|
||
patternInterval *= phase2IntervalMult; // 줄일거에요 -> 공격 간격을
|
||
|
||
_isPerformingAction = false; // 해제할거에요 -> 행동 차단을
|
||
|
||
Debug.Log($"[Boss] Phase2 완료 — 간격 {patternInterval:F1}s / 속도 ×{phase2SpeedMult}"); // 로그를 찍을거에요
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 애니메이션 이벤트 수신 함수
|
||
// FBX → Animations 탭 → 클립 → Events에 함수명으로 등록
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
public void OnAnimEnd() // 함수를 선언할거에요 -> 애니 끝 신호를 받는
|
||
{
|
||
_animEndFired = true; // 설정할거에요 -> 종료 플래그를
|
||
Debug.Log("[BossEvent] OnAnimEnd"); // 로그를 찍을거에요
|
||
}
|
||
|
||
public void OnThrowRelease() // 함수를 선언할거에요 -> 공 손 떠나는 순간 신호를 받는
|
||
{
|
||
_throwFired = true; // 설정할거에요 -> 투척 플래그를
|
||
Debug.Log("[BossEvent] OnThrowRelease"); // 로그를 찍을거에요
|
||
}
|
||
|
||
public void OnSmashHit() // 함수를 선언할거에요 -> 찍기 타격 순간 신호를 받는
|
||
{
|
||
_smashHitFired = true; // 설정할거에요 -> 찍기 판정 플래그를
|
||
Debug.Log("[BossEvent] OnSmashHit"); // 로그를 찍을거에요
|
||
}
|
||
|
||
public void OnSweepHit() // 함수를 선언할거에요 -> 휩쓸기 타격 순간 신호를 받는 (즉시 판정)
|
||
{
|
||
float dmg = sweepDamage * (_isPhase2 ? phase2DamageMult : 1f); // 계산할거에요 -> 페이즈 보정 데미지를
|
||
ApplyAreaDamage(sweepRadius, dmg, sweepHeightOffset, "휩쓸기"); // 실행할거에요 -> 즉시 범위 데미지를
|
||
Debug.Log("[BossEvent] OnSweepHit"); // 로그를 찍을거에요
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// AI 로직 (매 프레임)
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
protected override void ExecuteAILogic() // 함수를 정의할거에요 -> 매 프레임 AI 행동을
|
||
{
|
||
if (_isPerformingAction || !_isBattleStarted || _target == null) return; // 중단할거에요 -> 행동중/시작전/타겟없으면
|
||
if (isAttacking || isHit || isResting) return; // 중단할거에요 -> 전투 중간 상태면
|
||
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> NavMesh 비정상이면
|
||
|
||
if (_isWeaponless) { RetrieveWeaponLogic(); return; } // 실행할거에요 -> 무기 없으면 회수 우선
|
||
|
||
float dist = Vector3.Distance(transform.position, _target.position); // 계산할거에요 -> 플레이어 거리를
|
||
|
||
// ── 도망 감지: dashChaseRange 이상 거리가 dashChaseDelay초 지속 → 대시 추격
|
||
if (dist >= dashChaseRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 충분히 멀면
|
||
{
|
||
_farFromBossTimer += Time.deltaTime; // 더할거에요 -> 누적 시간을
|
||
if (_farFromBossTimer >= dashChaseDelay) // 조건이 맞으면 실행할거에요 -> 일정 시간 이상 멀었으면
|
||
{
|
||
_farFromBossTimer = 0f; // 초기화할거에요 -> 누적 시간을
|
||
_attackTimer = patternInterval; // 재설정할거에요 -> 쿨타임을
|
||
Debug.Log("[Boss] 도망 감지 → 대시 추격!"); // 로그를 찍을거에요
|
||
StartCoroutine(Pattern_Dash()); // 시작할거에요 -> 대시 추격을
|
||
return; // 중단할거에요 -> 이번 프레임 추가 처리 중단
|
||
}
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> 가까우면
|
||
{
|
||
_farFromBossTimer = 0f; // 초기화할거에요 -> 도망 누적 시간을
|
||
}
|
||
|
||
// ── 근접 범위 안: 멈추고 공격 쿨타임 카운트
|
||
if (dist <= meleeRange) // 조건이 맞으면 실행할거에요 -> 근접 범위 안이면
|
||
{
|
||
agent.isStopped = true; // 멈출거에요 -> 이동을
|
||
agent.velocity = Vector3.zero; // 제거할거에요 -> 관성을
|
||
LookAt(_target.position); // 바라볼거에요 -> 플레이어를
|
||
|
||
if (animator.GetCurrentAnimatorStateInfo(0).IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷는 중이면
|
||
animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니로
|
||
|
||
_attackTimer -= Time.deltaTime; // 줄일거에요 -> 쿨타임을
|
||
if (_attackTimer <= 0f) // 조건이 맞으면 실행할거에요 -> 쿨타임 끝났으면
|
||
{
|
||
_attackTimer = patternInterval; // 재설정할거에요 -> 쿨타임을
|
||
SelectAndExecutePattern(dist); // 실행할거에요 -> 거리 기반 패턴 선택을
|
||
}
|
||
}
|
||
// ── 원거리 범위: 쫓아가다가 throwRange 이하로 가까우면 던지기
|
||
else if (dist <= throwRange) // 조건이 맞으면 실행할거에요 -> 던지기 사거리 안이면
|
||
{
|
||
agent.isStopped = true; // 멈출거에요 -> 이동 멈추고
|
||
agent.velocity = Vector3.zero; // 제거할거에요 -> 관성을
|
||
LookAt(_target.position); // 바라볼거에요 -> 플레이어를
|
||
|
||
if (animator.GetCurrentAnimatorStateInfo(0).IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷는 중이면
|
||
animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니로
|
||
|
||
_attackTimer -= Time.deltaTime; // 줄일거에요 -> 쿨타임을
|
||
if (_attackTimer <= 0f) // 조건이 맞으면 실행할거에요 -> 쿨타임 끝났으면
|
||
{
|
||
_attackTimer = patternInterval; // 재설정할거에요 -> 쿨타임을
|
||
SelectAndExecutePattern(dist); // 실행할거에요 -> 거리 기반 패턴 선택을
|
||
}
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> 던지기 사거리 밖이면 추격
|
||
{
|
||
agent.isStopped = false; // 켤거에요 -> 이동을
|
||
agent.SetDestination(_target.position); // 설정할거에요 -> 목적지를
|
||
LookAt(_target.position); // 바라볼거에요 -> 플레이어를
|
||
UpdateMoveAnim(); // 갱신할거에요 -> 이동 애니를
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 패턴 선택
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void SelectAndExecutePattern(float dist) // 함수를 선언할거에요 -> 거리에 따라 패턴을 결정하고 실행하는
|
||
{
|
||
LookAt(_target.position); // 바라볼거에요 -> 공격 전 방향 정렬을
|
||
|
||
BossPattern pattern; // 변수를 선언할거에요 -> 선택된 패턴을
|
||
|
||
bool isNearby = dist <= meleeRange; // 판단할거에요 -> 근접 범위 안인지를
|
||
bool isFar = dist > meleeRange; // 판단할거에요 -> 원거리 범위인지를
|
||
|
||
if (_isPhase2) // 조건이 맞으면 실행할거에요 -> Phase2이면
|
||
{
|
||
if (isNearby) // 조건이 맞으면 실행할거에요 -> 근접 범위이면
|
||
{
|
||
// 근접: 찍기/휩쓸기/콤보 위주
|
||
int roll = Random.Range(0, 10); // 뽑을거에요 -> 랜덤 값을
|
||
if (roll < 3) pattern = BossPattern.ComboSmash; // 30% — 연속 스매쉬
|
||
else if (roll < 6) pattern = BossPattern.DashSmash; // 30% — 돌진+찍기
|
||
else if (roll < 8) pattern = BossPattern.Smash; // 20% — 찍기
|
||
else pattern = BossPattern.Sweep; // 20% — 휩쓸기
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> 원거리이면
|
||
{
|
||
// 원거리: 던지기/대시 위주
|
||
int roll = Random.Range(0, 10); // 뽑을거에요 -> 랜덤 값을
|
||
if (roll < 5) pattern = BossPattern.Throw; // 50% — 던지기
|
||
else pattern = BossPattern.Dash; // 50% — 돌진 추격
|
||
}
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> Phase1이면
|
||
{
|
||
if (isFar) // 조건이 맞으면 실행할거에요 -> 원거리이면 무조건 던지기
|
||
{
|
||
pattern = BossPattern.Throw; // 설정할거에요 -> 던지기를
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> 근접이면 카운터 시스템 활용
|
||
{
|
||
string result = counterSystem != null ? counterSystem.SelectBossPattern() : "Normal"; // 선택할거에요 -> 카운터 패턴을
|
||
switch (result) // 분기할거에요 -> 결과에 따라
|
||
{
|
||
case "DashAttack": pattern = BossPattern.Dash; break; // 설정할거에요 -> 돌진을
|
||
case "Smash": pattern = BossPattern.Smash; break; // 설정할거에요 -> 찍기를
|
||
case "ShieldWall": pattern = BossPattern.Sweep; break; // 설정할거에요 -> 휩쓸기를
|
||
default: pattern = BossPattern.Smash; break; // 설정할거에요 -> 기본 근접: 찍기를
|
||
}
|
||
}
|
||
}
|
||
|
||
Debug.Log($"[Boss] 패턴={pattern} dist={dist:F1}m Phase2={_isPhase2}"); // 로그를 찍을거에요
|
||
|
||
switch (pattern) // 분기할거에요 -> 패턴에 따라 코루틴 시작
|
||
{
|
||
case BossPattern.Throw: StartCoroutine(Pattern_Throw()); break;
|
||
case BossPattern.Smash: StartCoroutine(Pattern_Smash()); break;
|
||
case BossPattern.Sweep: StartCoroutine(Pattern_Sweep()); break;
|
||
case BossPattern.Dash: StartCoroutine(Pattern_Dash()); break;
|
||
case BossPattern.ComboSmash: StartCoroutine(Pattern_ComboSmash()); break;
|
||
case BossPattern.DashSmash: StartCoroutine(Pattern_DashSmash()); break;
|
||
default: StartCoroutine(Pattern_Throw()); break;
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 전투 시작
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
public void StartBossBattle() // 함수를 선언할거에요 -> 보스전을 시작하는
|
||
{
|
||
if (_isBattleStarted) return; // 중단할거에요 -> 이미 시작됐으면
|
||
StartCoroutine(BattleStartRoutine()); // 시작할거에요 -> 전투 시작 코루틴을
|
||
}
|
||
|
||
private IEnumerator BattleStartRoutine() // 코루틴을 정의할거에요 -> 전투 시작 연출을
|
||
{
|
||
_isPerformingAction = true; // 잠글거에요 -> 행동을
|
||
|
||
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 켤거에요 -> 체력바를
|
||
if (counterSystem != null) counterSystem.InitializeBattle(); // 초기화할거에요 -> 카운터 시스템을
|
||
|
||
yield return StartCoroutine(PlayAndWait(anim_Roar, 10f)); // 기다릴거에요 -> 포효 종료를
|
||
|
||
animator.Play(anim_Idle, 0, 0f); // 재생할거에요 -> 대기 애니를
|
||
_isPerformingAction = false; // 해제할거에요 -> 행동 잠금을
|
||
_isBattleStarted = true; // 설정할거에요 -> 전투 시작 상태로
|
||
|
||
if (agent != null) // 조건이 맞으면 실행할거에요 -> 에이전트 있으면
|
||
{
|
||
agent.enabled = true; // 켤거에요 -> 에이전트를
|
||
int retry = 0; // 초기화할거에요 -> 재시도 횟수를
|
||
while (!agent.isOnNavMesh && retry < 10) { retry++; yield return new WaitForSeconds(0.1f); } // 기다릴거에요 -> NavMesh 위에 올 때까지
|
||
if (agent.isOnNavMesh) agent.isStopped = false; // 켤거에요 -> 이동을
|
||
else Debug.LogError("[Boss] NavMesh 연결 실패!"); // 에러를 찍을거에요
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 패턴 ① — 쇠공 던지기
|
||
//
|
||
// ✅ 버그① 수정 포인트:
|
||
// SetParent(null) 직전 월드 좌표 저장 → 분리 후 위치 복원
|
||
// → 포물선 초기속도 계산으로 정확히 날아감
|
||
// → 착지 후 velocity < 0.5 감지 → 범위 데미지
|
||
//
|
||
// 이벤트: Attack_Throw → OnThrowRelease (손 떠나는 프레임)
|
||
// Attack_Throw → OnAnimEnd (마지막 프레임)
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
private IEnumerator Pattern_Throw() // 코루틴을 정의할거에요 -> 쇠공 던지기 패턴을
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
||
|
||
if (ironBall == null) // 조건이 맞으면 실행할거에요 -> 쇠공 없으면
|
||
{
|
||
Debug.LogError("[Boss] Iron Ball 슬롯이 비어있어요! Inspector에서 연결 필요"); // 에러를 찍을거에요
|
||
OnAttackEnd(); // 실행할거에요 -> 패턴 종료를
|
||
yield break; // 중단할거에요
|
||
}
|
||
|
||
// 발사 목표 미리 고정 (애니 재생 전에 저장 → 도중에 플레이어가 움직여도 OK)
|
||
Vector3 throwTarget = _target != null
|
||
? _target.position + Vector3.up * 0.8f // 저장할거에요 -> 플레이어 허리 높이를
|
||
: transform.position + transform.forward * 10f; // 저장할거에요 -> 타겟 없으면 전방을
|
||
|
||
// 던지기 애니 재생
|
||
if (animator != null && animator.HasState(0, Animator.StringToHash(anim_Throw))) // 조건이 맞으면 실행할거에요 -> State 있으면
|
||
animator.Play(anim_Throw, 0, 0f); // 재생할거에요 -> 던지기 애니를
|
||
yield return null; // 기다릴거에요 -> 1프레임 (상태 전환)
|
||
|
||
// 던지기 애니 길이의 40% 시점에 공 발사 (이벤트 없어도 동작)
|
||
float clipLen = GetCurrentClipLength(); // 가져올거에요 -> 현재 클립 길이를
|
||
float throwTime = Mathf.Max(clipLen * 0.4f, 0.3f); // 계산할거에요 -> 발사 타이밍을
|
||
|
||
// OnThrowRelease 이벤트 OR 타이머 중 먼저 오는 것으로 발사
|
||
_throwFired = false; // 초기화할거에요 -> 이전 신호를
|
||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
while (!_throwFired && elapsed < throwTime) // 기다릴거에요 -> 이벤트 또는 타이머
|
||
{
|
||
elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
}
|
||
|
||
// ── 공 발사 ──
|
||
yield return StartCoroutine(DoLaunchBall(throwTarget)); // 실행할거에요 -> 공 발사 코루틴을
|
||
|
||
_isWeaponless = true; // 설정할거에요 -> 무기 없음 상태로
|
||
|
||
// 나머지 애니 재생 대기 (이미 지난 시간 빼고)
|
||
float remaining = Mathf.Max(clipLen - elapsed - 0.1f, 0.2f); // 계산할거에요 -> 남은 시간을
|
||
yield return new WaitForSeconds(remaining); // 기다릴거에요 -> 애니 끝날 때까지
|
||
|
||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
||
}
|
||
|
||
private IEnumerator DoLaunchBall(Vector3 targetPos) // 코루틴을 정의할거에요 -> 쇠공을 실제로 발사하는
|
||
{
|
||
// ── 발사 위치 확정 ──
|
||
Vector3 launchPos = ironBall.transform.position; // 저장할거에요 -> 발사 월드 위치를
|
||
|
||
// ── 손에서 분리 ──
|
||
ironBall.transform.SetParent(null); // 분리할거에요 -> 손에서
|
||
ironBall.transform.position = launchPos; // 복원할거에요 -> SetParent로 틀어진 위치를
|
||
|
||
// ── Rigidbody 가져오기 (발사 시점에 직접 → null 캐싱 문제 없음) ──
|
||
Rigidbody rb = ironBall.GetComponent<Rigidbody>(); // 가져올거에요 -> Rigidbody를 발사 시점에
|
||
|
||
if (rb != null) // 조건이 맞으면 실행할거에요 -> Rigidbody 있으면 물리 발사
|
||
{
|
||
rb.isKinematic = false; // 해제할거에요 -> 물리를
|
||
rb.velocity = Vector3.zero; // 초기화할거에요 -> 이전 속도를
|
||
rb.angularVelocity = Vector3.zero; // 초기화할거에요 -> 회전 속도를
|
||
|
||
yield return new WaitForFixedUpdate(); // 기다릴거에요 -> FixedUpdate 1회 (물리 활성화 대기)
|
||
yield return new WaitForFixedUpdate(); // 기다릴거에요 -> 한 번 더 (안전 마진)
|
||
|
||
Vector3 vel = CalcParabolaVelocity(launchPos, targetPos, throwArcHeight); // 계산할거에요 -> 포물선 초기 속도를
|
||
rb.velocity = vel; // 설정할거에요 -> 속도를 직접
|
||
|
||
Debug.Log($"[Boss] ✅ 쇠공 물리 발사! vel={vel} 목표={targetPos}"); // 로그를 찍을거에요
|
||
StartCoroutine(BallLandingDamage(rb)); // 시작할거에요 -> 착지 판정을
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> Rigidbody 없으면 코드로 직접 이동
|
||
{
|
||
Debug.LogWarning("[Boss] 쇠공에 Rigidbody 없음 → 코드 이동 방식 사용"); // 경고를 찍을거에요
|
||
StartCoroutine(MoveBallManually(launchPos, targetPos)); // 시작할거에요 -> 수동 이동 코루틴을
|
||
}
|
||
}
|
||
|
||
private IEnumerator MoveBallManually(Vector3 from, Vector3 to) // 코루틴을 정의할거에요 -> Rigidbody 없이 쇠공을 포물선으로 직접 이동하는
|
||
{
|
||
float duration = 1.2f; // 설정할거에요 -> 비행 시간을
|
||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
|
||
while (elapsed < duration && ironBall != null) // 반복할거에요 -> 비행 시간 동안
|
||
{
|
||
elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
|
||
float t = elapsed / duration; // 계산할거에요 -> 진행 비율을 (0~1)
|
||
|
||
// 포물선 보간: 수평은 Lerp, 수직은 포물선 커브
|
||
Vector3 pos = Vector3.Lerp(from, to, t); // 계산할거에요 -> 수평 위치를
|
||
pos.y += throwArcHeight * 4f * t * (1f - t); // 더할거에요 -> 포물선 높이를 (t=0.5에서 최고)
|
||
|
||
ironBall.transform.position = pos; // 이동할거에요 -> 쇠공을
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
}
|
||
|
||
if (ironBall != null) ironBall.transform.position = to; // 이동할거에요 -> 최종 목표 위치로
|
||
|
||
// 착지 범위 데미지
|
||
float dmg = ballLandDamage * (_isPhase2 ? phase2DamageMult : 1f); // 계산할거에요 -> 데미지를
|
||
Collider[] hits = Physics.OverlapSphere(to, ballLandRadius); // 검사할거에요 -> 착지 범위를
|
||
foreach (Collider hit in hits) // 반복할거에요 -> 감지된 콜라이더를
|
||
{
|
||
if (!hit.CompareTag("Player")) continue; // 건너뛸거에요 -> 플레이어 아니면
|
||
IDamageable d = hit.GetComponent<IDamageable>() ?? hit.GetComponentInParent<IDamageable>(); // 가져올거에요 -> 데미지 인터페이스를
|
||
if (d != null) d.TakeDamage(dmg); // 줄거에요 -> 데미지를
|
||
}
|
||
Debug.Log($"[Boss] 쇠공 착지 (수동 이동) 위치={to}"); // 로그를 찍을거에요
|
||
}
|
||
|
||
private Vector3 CalcParabolaVelocity(Vector3 from, Vector3 to, float arcH) // 함수를 선언할거에요 -> 포물선 초기 속도를 계산하는
|
||
{
|
||
float g = Mathf.Abs(Physics.gravity.y); // 가져올거에요 -> 중력 크기를
|
||
float dx = Vector3.Distance(
|
||
new Vector3(from.x, 0, from.z),
|
||
new Vector3(to.x, 0, to.z)); // 계산할거에요 -> 수평 거리를
|
||
float dy = to.y - from.y; // 계산할거에요 -> 수직 거리를
|
||
|
||
float tUp = Mathf.Sqrt(2f * arcH / g); // 계산할거에요 -> 상승 시간을
|
||
float tDown = Mathf.Sqrt(2f * Mathf.Max(arcH - dy, 0.1f) / g); // 계산할거에요 -> 하강 시간을
|
||
float tTotal = Mathf.Max(tUp + tDown, 0.1f); // 계산할거에요 -> 총 비행 시간을
|
||
|
||
Vector3 hDir = (to - from); // 계산할거에요 -> 수평 방향을
|
||
hDir.y = 0f; // 제거할거에요 -> 수직 성분을
|
||
hDir = hDir.normalized * (dx / tTotal); // 계산할거에요 -> 수평 속도를
|
||
|
||
float vy = arcH / tUp; // 계산할거에요 -> 초기 수직 속도를
|
||
|
||
return new Vector3(hDir.x, vy, hDir.z); // 반환할거에요 -> 포물선 초기 속도를
|
||
}
|
||
|
||
private IEnumerator BallLandingDamage(Rigidbody rb) // 코루틴을 정의할거에요 -> 쇠공 착지 후 범위 데미지를
|
||
{
|
||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
|
||
while (elapsed < 6f) // 반복할거에요 -> 최대 6초 동안 착지 감지
|
||
{
|
||
elapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
|
||
// 속도가 충분히 줄어들면 착지로 판정
|
||
if (rb != null && elapsed > 0.4f && rb.velocity.magnitude < 1f) break; // 중단할거에요 -> 착지 감지되면
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
}
|
||
|
||
if (ironBall == null) yield break; // 중단할거에요 -> 쇠공 없으면
|
||
|
||
float dmg = ballLandDamage * (_isPhase2 ? phase2DamageMult : 1f); // 계산할거에요 -> 보정 데미지를
|
||
Collider[] hits = Physics.OverlapSphere(ironBall.transform.position, ballLandRadius); // 검사할거에요 -> 착지 위치 기준으로
|
||
foreach (Collider hit in hits) // 반복할거에요 -> 감지된 콜라이더를
|
||
{
|
||
if (!hit.CompareTag("Player")) continue; // 건너뛸거에요 -> 플레이어 아니면
|
||
IDamageable d = hit.GetComponent<IDamageable>() ?? hit.GetComponentInParent<IDamageable>(); // 가져올거에요 -> 데미지 인터페이스를
|
||
if (d != null) { d.TakeDamage(dmg); Debug.Log($"[Boss] 쇠공 착지 {dmg:F0} 데미지"); } // 줄거에요 -> 데미지를
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 패턴 ② — 내려찍기
|
||
//
|
||
// ✅ 버그③ 수정: smashHeightOffset으로 판정 중심 높이 보정
|
||
//
|
||
// 이벤트: Skill_Smash_Impact → OnSmashHit (바닥 닿는 프레임)
|
||
// Skill_Smash_Impact → OnAnimEnd (마지막 프레임)
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
private IEnumerator Pattern_Smash() // 코루틴을 정의할거에요 -> 내려찍기 패턴을
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
||
_smashHitFired = false; // 초기화할거에요 -> 찍기 플래그를
|
||
_animEndFired = false; // 초기화할거에요 -> 종료 플래그를
|
||
|
||
yield return StartCoroutine(PlayAnimSafe(anim_SmashImpact)); // 재생할거에요 -> 찍기 애니를
|
||
|
||
yield return StartCoroutine(WaitSmashHit(8f, "Smash")); // 기다릴거에요 -> 찍기 신호를
|
||
|
||
float dmg = smashDamage * (_isPhase2 ? phase2DamageMult : 1f); // 계산할거에요 -> 보정 데미지를
|
||
ApplyAreaDamage(smashRadius, dmg, smashHeightOffset, "내려찍기"); // 실행할거에요 -> 높이 보정 범위 데미지를 ← 버그③ 수정
|
||
|
||
yield return StartCoroutine(WaitAnimEnd(8f, "Smash")); // 기다릴거에요 -> 종료 신호를
|
||
|
||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 패턴 ③ — 휩쓸기
|
||
// 데미지: OnSweepHit 애니메이션 이벤트에서 즉시 처리
|
||
//
|
||
// 이벤트: Skill_Sweep → OnSweepHit (쓸리는 프레임)
|
||
// Skill_Sweep → OnAnimEnd (마지막 프레임)
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
private IEnumerator Pattern_Sweep() // 코루틴을 정의할거에요 -> 휩쓸기 패턴을
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
||
yield return StartCoroutine(PlayAndWait(anim_Sweep, 8f)); // 기다릴거에요 -> 휩쓸기 애니 종료를
|
||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 패턴 ④ — 돌진
|
||
//
|
||
// ✅ 버그② 수정: OnDashStart 이벤트 의존 완전 제거
|
||
// dashReadyDuration 타이머로 준비 시간 대기 후 즉시 물리 이동
|
||
// rb.isKinematic = false 명시 → velocity 적용 보장
|
||
//
|
||
// 이벤트: 없음 (이벤트 불필요, 타이머 방식)
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
private IEnumerator Pattern_Dash() // 코루틴을 정의할거에요 -> 돌진 패턴을
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
||
if (agent != null) agent.enabled = false; // 끌거에요 -> NavMesh를 (물리 이동 위해)
|
||
|
||
// ① 준비 애니 재생
|
||
if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 재생할거에요 -> 준비 애니를
|
||
|
||
// ② 준비 시간 동안 플레이어 방향으로 빠르게 회전 (방향 계속 갱신)
|
||
float readyElapsed = 0f; // 초기화할거에요 -> 준비 경과 시간을
|
||
while (readyElapsed < dashReadyDuration) // 반복할거에요 -> 준비 시간 동안
|
||
{
|
||
readyElapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
|
||
if (_target != null) // 조건이 맞으면 실행할거에요 -> 타겟 있으면
|
||
{
|
||
Vector3 toTarget = _target.position - transform.position; // 계산할거에요 -> 플레이어 방향을
|
||
toTarget.y = 0f; // 제거할거에요 -> 높이 차이를
|
||
if (toTarget.sqrMagnitude > 0.001f) // 조건이 맞으면 실행할거에요 -> 방향 있으면
|
||
transform.rotation = Quaternion.Slerp(
|
||
transform.rotation,
|
||
Quaternion.LookRotation(toTarget),
|
||
Time.deltaTime * 15f); // 회전할거에요 -> 빠르게 플레이어 방향으로 (일반 LookAt보다 3배 빠름)
|
||
}
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
}
|
||
|
||
// ③ 돌진 직전 방향 최종 확정 + 즉시 스냅 (Slerp 아닌 즉시 적용)
|
||
Vector3 dashDir = _target != null
|
||
? (new Vector3(_target.position.x, transform.position.y, _target.position.z) - transform.position).normalized // 계산할거에요 -> 최신 플레이어 위치 기준 방향을
|
||
: transform.forward; // 사용할거에요 -> 타겟 없으면 전방을
|
||
if (dashDir.sqrMagnitude > 0.001f) // 조건이 맞으면 실행할거에요 -> 방향 있으면
|
||
transform.rotation = Quaternion.LookRotation(dashDir); // 설정할거에요 -> 즉시 방향 확정
|
||
|
||
// ④ 물리 이동 시작
|
||
if (_rb != null) // 조건이 맞으면 실행할거에요 -> Rigidbody 있으면
|
||
{
|
||
_rb.isKinematic = false; // 켤거에요 -> 물리를
|
||
_rb.velocity = Vector3.zero; // 초기화할거에요 -> 이전 속도를
|
||
_rb.velocity = dashDir * dashSpeed; // 설정할거에요 -> 정확한 방향으로 돌진을
|
||
}
|
||
else // 조건이 틀리면 실행할거에요 -> Rigidbody 없으면 NavMesh 이동으로 대체
|
||
{
|
||
Debug.LogWarning("[Boss] 보스에 Rigidbody 없음 → NavMesh 대시 이동으로 대체"); // 경고를 찍을거에요
|
||
if (agent != null)
|
||
{
|
||
agent.enabled = true; // 켤거에요 -> NavMesh를
|
||
yield return new WaitForSeconds(0.05f); // 기다릴거에요 -> 활성화 대기를
|
||
if (agent.isOnNavMesh)
|
||
{
|
||
agent.speed = dashSpeed; // 설정할거에요 -> 대시 속도를
|
||
agent.isStopped = false; // 켤거에요 -> 이동을
|
||
agent.SetDestination(_target != null ? _target.position : transform.position + transform.forward * 5f); // 설정할거에요 -> 목적지를
|
||
}
|
||
}
|
||
}
|
||
_isDashing = true; // 설정할거에요 -> 돌진 중으로 (충돌 데미지 활성화)
|
||
|
||
// ⑤ 돌진 애니 재생
|
||
if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 재생할거에요 -> 돌진 애니를
|
||
|
||
// ⑥ 돌진 지속 시간 대기
|
||
yield return new WaitForSeconds(dashDuration); // 기다릴거에요 -> 돌진 시간만큼
|
||
|
||
// ⑦ 돌진 종료
|
||
_isDashing = false; // 해제할거에요 -> 돌진 상태를
|
||
if (_rb != null)
|
||
{
|
||
_rb.velocity = Vector3.zero; // 멈출거에요 -> 속도를
|
||
_rb.isKinematic = true; // 고정할거에요 -> 물리를
|
||
}
|
||
if (agent != null && _isBattleStarted)
|
||
{
|
||
agent.enabled = true; // 켤거에요 -> NavMesh를
|
||
if (agent.isOnNavMesh) agent.speed = moveSpeed; // 복구할거에요 -> 원래 이동 속도로
|
||
yield return new WaitForSeconds(0.05f); // 기다릴거에요 -> 재활성화 안정 대기
|
||
}
|
||
|
||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 패턴 ⑤ — 연속 스매쉬 (Phase2 전용)
|
||
// 찍기를 0.3초 간격으로 2회 연속
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
private IEnumerator Pattern_ComboSmash() // 코루틴을 정의할거에요 -> 연속 찍기 패턴을 (Phase2 전용)
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
||
|
||
for (int i = 0; i < 2; i++) // 반복할거에요 -> 2회를
|
||
{
|
||
_smashHitFired = false; // 초기화할거에요 -> 찍기 플래그를
|
||
_animEndFired = false; // 초기화할거에요 -> 종료 플래그를
|
||
|
||
if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 재생할거에요 -> 찍기 애니를
|
||
yield return null; // 기다릴거에요 -> 1프레임 (상태 전환 대기)
|
||
|
||
yield return StartCoroutine(WaitSmashHit(6f, $"ComboSmash({i + 1})")); // 기다릴거에요 -> 찍기 신호를
|
||
|
||
float dmg = smashDamage * phase2DamageMult; // 계산할거에요 -> Phase2 강화 데미지를
|
||
ApplyAreaDamage(smashRadius * 1.2f, dmg, smashHeightOffset, $"연속찍기 {i + 1}번"); // 실행할거에요 -> 범위 데미지를 (범위 20% 확대)
|
||
|
||
yield return StartCoroutine(WaitAnimEnd(6f, $"ComboSmash({i + 1})")); // 기다릴거에요 -> 종료 신호를
|
||
|
||
if (i == 0) yield return new WaitForSeconds(0.3f); // 기다릴거에요 -> 2번째 찍기 전 짧은 딜레이
|
||
}
|
||
|
||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// 패턴 ⑥ — 돌진 + 찍기 콤보 (Phase2 전용)
|
||
// 빠른 돌진으로 플레이어 위치까지 이동 → 즉시 찍기 연계
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
private IEnumerator Pattern_DashSmash() // 코루틴을 정의할거에요 -> 돌진+찍기 콤보 패턴을 (Phase2 전용)
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 공격 시작 처리를
|
||
if (agent != null) agent.enabled = false; // 끌거에요 -> NavMesh를
|
||
|
||
// ── 돌진 파트 ──────────────────────────────────────
|
||
if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 재생할거에요 -> 준비 애니를
|
||
|
||
// 준비 시간(70%)동안 플레이어 방향으로 실시간 회전
|
||
float dsReady = dashReadyDuration * 0.7f; // 계산할거에요 -> 콤보 준비 시간을
|
||
float dsElapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
while (dsElapsed < dsReady) // 반복할거에요 -> 준비 시간 동안
|
||
{
|
||
dsElapsed += Time.deltaTime; // 더할거에요 -> 경과 시간을
|
||
if (_target != null) // 조건이 맞으면 실행할거에요 -> 타겟 있으면
|
||
{
|
||
Vector3 toT = _target.position - transform.position; // 계산할거에요 -> 방향을
|
||
toT.y = 0f; // 제거할거에요 -> 높이를
|
||
if (toT.sqrMagnitude > 0.001f)
|
||
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(toT), Time.deltaTime * 15f); // 회전할거에요 -> 빠르게
|
||
}
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
}
|
||
|
||
// 발사 직전 방향 즉시 확정
|
||
Vector3 dashDir = _target != null
|
||
? (new Vector3(_target.position.x, transform.position.y, _target.position.z) - transform.position).normalized // 계산할거에요 -> 최신 방향을
|
||
: transform.forward; // 사용할거에요 -> 기본 방향을
|
||
if (dashDir.sqrMagnitude > 0.001f)
|
||
transform.rotation = Quaternion.LookRotation(dashDir); // 설정할거에요 -> 즉시 스냅
|
||
|
||
if (_rb != null)
|
||
{
|
||
_rb.isKinematic = false; // 켤거에요 -> 물리를
|
||
_rb.velocity = dashDir * dashSpeed * 1.2f; // 설정할거에요 -> 가속 돌진을
|
||
}
|
||
_isDashing = true; // 설정할거에요 -> 돌진 중으로
|
||
if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 재생할거에요 -> 돌진 애니를
|
||
|
||
yield return new WaitForSeconds(dashDuration * 0.8f); // 기다릴거에요 -> 돌진 시간 80%만 이동 후 찍기 연계
|
||
|
||
// 돌진 중단
|
||
_isDashing = false; // 해제할거에요 -> 돌진 상태를
|
||
if (_rb != null) { _rb.velocity = Vector3.zero; _rb.isKinematic = true; } // 멈출거에요 -> 속도를
|
||
|
||
// ── 찍기 파트 ──────────────────────────────────────
|
||
LookAt(_target != null ? _target.position : transform.position + transform.forward); // 바라볼거에요 -> 플레이어를
|
||
|
||
_smashHitFired = false; // 초기화할거에요 -> 찍기 플래그를
|
||
_animEndFired = false; // 초기화할거에요 -> 종료 플래그를
|
||
|
||
if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 재생할거에요 -> 찍기 애니를
|
||
yield return null; // 기다릴거에요 -> 1프레임을
|
||
|
||
yield return StartCoroutine(WaitSmashHit(6f, "DashSmash")); // 기다릴거에요 -> 찍기 신호를
|
||
|
||
float dmg = smashDamage * phase2DamageMult * 1.3f; // 계산할거에요 -> 콤보 보너스 데미지를
|
||
ApplyAreaDamage(smashRadius * 1.3f, dmg, smashHeightOffset, "돌진+찍기 콤보"); // 실행할거에요 -> 범위 데미지를 (범위 30% 확대)
|
||
|
||
yield return StartCoroutine(WaitAnimEnd(6f, "DashSmash")); // 기다릴거에요 -> 종료 신호를
|
||
|
||
if (agent != null && _isBattleStarted) { agent.enabled = true; yield return new WaitForSeconds(0.05f); } // 켤거에요 -> NavMesh를
|
||
|
||
OnAttackEnd(); // 실행할거에요 -> 공격 종료 처리를
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 범위 데미지 공통 함수
|
||
// ✅ 버그③ 수정: heightOffset으로 판정 중심 높이 보정
|
||
// GetComponentInParent 추가로 계층 구조 대응
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void ApplyAreaDamage(float radius, float damage, float heightOffset, string attackName) // 함수를 선언할거에요 -> 범위 안 플레이어에게 데미지를 주는
|
||
{
|
||
Vector3 center = transform.position + Vector3.up * heightOffset; // 계산할거에요 -> 보정된 판정 중심을 ← 핵심 수정
|
||
|
||
Collider[] hits = Physics.OverlapSphere(center, radius); // 검사할거에요 -> 반경 안의 콜라이더를
|
||
foreach (Collider hit in hits) // 반복할거에요 -> 감지된 콜라이더를
|
||
{
|
||
if (!hit.CompareTag("Player")) continue; // 건너뛸거에요 -> 플레이어 아니면
|
||
|
||
// ✅ 자신 + 부모까지 탐색 (계층 구조 대응)
|
||
IDamageable dmg = hit.GetComponent<IDamageable>()
|
||
?? hit.GetComponentInParent<IDamageable>(); // 가져올거에요 -> 데미지 인터페이스를
|
||
|
||
if (dmg != null)
|
||
{
|
||
dmg.TakeDamage(damage); // 줄거에요 -> 데미지를
|
||
Debug.Log($"[Boss] {attackName} 적중! {damage:F0} 데미지"); // 로그를 찍을거에요
|
||
}
|
||
else Debug.LogWarning($"[Boss] {attackName}: Player에 IDamageable 없음!"); // 경고를 찍을거에요
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 돌진 충돌 판정
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void OnCollisionEnter(Collision col) // 함수를 실행할거에요 -> 물리 충돌 감지 시
|
||
{
|
||
if (!_isDashing) return; // 중단할거에요 -> 돌진 중 아니면
|
||
if (!col.gameObject.CompareTag("Player")) return; // 중단할거에요 -> 플레이어 아니면
|
||
|
||
IDamageable dmg = col.gameObject.GetComponent<IDamageable>()
|
||
?? col.gameObject.GetComponentInParent<IDamageable>(); // 가져올거에요 -> 데미지 인터페이스를
|
||
|
||
if (dmg != null)
|
||
{
|
||
float finalDmg = dashDamage * (_isPhase2 ? phase2DamageMult : 1f); // 계산할거에요 -> 보정 데미지를
|
||
dmg.TakeDamage(finalDmg); // 줄거에요 -> 돌진 충돌 데미지를
|
||
Debug.Log($"[Boss] 돌진 충돌! {finalDmg:F0} 데미지"); // 로그를 찍을거에요
|
||
}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 쇠공 회수
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 쇠공 위치로 이동 후 줍는
|
||
{
|
||
if (ironBall == null || agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 중단할거에요 -> 조건 비정상이면
|
||
|
||
agent.SetDestination(ironBall.transform.position); // 이동할거에요 -> 쇠공 위치로
|
||
agent.isStopped = false; // 켤거에요 -> 이동을
|
||
UpdateMoveAnim(); // 갱신할거에요 -> 이동 애니를
|
||
|
||
float dist = Vector3.Distance(transform.position, ironBall.transform.position); // 계산할거에요 -> 쇠공 거리를
|
||
if (dist <= 2.5f && !isAttacking) StartCoroutine(PickUpRoutine()); // 실행할거에요 -> 줍기 코루틴을
|
||
}
|
||
|
||
private IEnumerator PickUpRoutine() // 코루틴을 정의할거에요 -> 쇠공 줍기를
|
||
{
|
||
OnAttackStart(); // 실행할거에요 -> 행동 잠금을
|
||
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 멈출거에요 -> 에이전트를
|
||
|
||
yield return StartCoroutine(PlayAndWait(anim_Pickup, 8f)); // 기다릴거에요 -> 줍기 애니 종료를
|
||
|
||
AttachBallToHand(); // 실행할거에요 -> 쇠공을 손에 부착 (오프셋 적용 + 물리 고정)
|
||
|
||
_isWeaponless = false; // 해제할거에요 -> 무기 없음 상태를
|
||
OnAttackEnd(); // 실행할거에요 -> 행동 해제를
|
||
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 재개할거에요 -> 이동을
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 유틸 코루틴
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private float GetCurrentClipLength() // 함수를 선언할거에요 -> 현재 재생 중인 클립 길이를 반환하는
|
||
{
|
||
if (animator == null) return 1f; // 반환할거에요 -> 기본값을
|
||
AnimatorClipInfo[] clips = animator.GetCurrentAnimatorClipInfo(0); // 가져올거에요 -> 현재 클립 정보를
|
||
if (clips.Length > 0) return clips[0].clip.length; // 반환할거에요 -> 클립 길이를
|
||
return 1f; // 반환할거에요 -> 클립 없으면 기본값을
|
||
}
|
||
|
||
/// <summary> 애니 재생 후 OnAnimEnd 대기 </summary>
|
||
private IEnumerator PlayAndWait(string animName, float maxWait = 10f) // 코루틴을 정의할거에요 -> 애니 재생 후 종료를 기다리는
|
||
{
|
||
yield return StartCoroutine(PlayAnimSafe(animName)); // 재생할거에요 -> 안전하게 애니를
|
||
yield return StartCoroutine(WaitAnimEnd(maxWait, animName)); // 기다릴거에요 -> 종료 신호를
|
||
}
|
||
|
||
/// <summary> Animator State 존재 확인 후 재생 </summary>
|
||
private IEnumerator PlayAnimSafe(string animName) // 코루틴을 정의할거에요 -> 안전하게 애니메이션을 재생하는
|
||
{
|
||
if (animator == null) yield break; // 중단할거에요 -> Animator 없으면
|
||
|
||
if (!animator.HasState(0, Animator.StringToHash(animName))) // 조건이 맞으면 실행할거에요 -> State 없으면
|
||
{
|
||
Debug.LogError($"[Boss] Animator State \"{animName}\" 없음! Inspector 이름 확인 필요"); // 에러를 찍을거에요
|
||
yield break; // 중단할거에요
|
||
}
|
||
|
||
_animEndFired = false; // 초기화할거에요 -> 이전 종료 신호를
|
||
animator.Play(animName, 0, 0f); // 재생할거에요 -> 해당 애니를
|
||
yield return null; // 기다릴거에요 -> 1프레임 (상태 전환 대기)
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// WaitFlag 3종 — ref 대신 각 플래그 전용 코루틴으로 분리
|
||
// (C# 이터레이터는 ref 파라미터 불가 → 컴파일 에러 CS1623 수정)
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private IEnumerator WaitAnimEnd(float maxWait, string label) // 코루틴을 정의할거에요 -> _animEndFired 플래그 대기를
|
||
{
|
||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
while (!_animEndFired && elapsed < maxWait) { elapsed += Time.deltaTime; yield return null; } // 기다릴거에요 -> 플래그 올 때까지
|
||
if (!_animEndFired) Debug.LogWarning($"[Boss] {label} OnAnimEnd 미수신 — 강제 진행"); // 경고를 찍을거에요
|
||
_animEndFired = false; // 초기화할거에요 -> 플래그를
|
||
}
|
||
|
||
private IEnumerator WaitThrowFired(float maxWait) // 코루틴을 정의할거에요 -> _throwFired 플래그 대기를
|
||
{
|
||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
while (!_throwFired && elapsed < maxWait) { elapsed += Time.deltaTime; yield return null; } // 기다릴거에요 -> 플래그 올 때까지
|
||
if (!_throwFired) Debug.LogWarning("[Boss] OnThrowRelease 미수신 — 강제 진행"); // 경고를 찍을거에요
|
||
_throwFired = false; // 초기화할거에요 -> 플래그를
|
||
}
|
||
|
||
private IEnumerator WaitSmashHit(float maxWait, string label) // 코루틴을 정의할거에요 -> _smashHitFired 플래그 대기를
|
||
{
|
||
float elapsed = 0f; // 초기화할거에요 -> 경과 시간을
|
||
while (!_smashHitFired && elapsed < maxWait) { elapsed += Time.deltaTime; yield return null; } // 기다릴거에요 -> 플래그 올 때까지
|
||
if (!_smashHitFired) Debug.LogWarning($"[Boss] {label} OnSmashHit 미수신 — 강제 진행"); // 경고를 찍을거에요
|
||
_smashHitFired = false; // 초기화할거에요 -> 플래그를
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 공격 시작 / 종료 / 회복
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
public override void OnAttackStart() // 함수를 정의할거에요 -> 공격 시작 공통 처리를
|
||
{
|
||
isAttacking = _isPerformingAction = true; // 설정할거에요 -> 공격/행동 중으로
|
||
if (agent != null && agent.isOnNavMesh) agent.isStopped = true; // 멈출거에요 -> 이동을
|
||
}
|
||
|
||
public override void OnAttackEnd() // 함수를 정의할거에요 -> 공격 종료 공통 처리를
|
||
{
|
||
StartCoroutine(RecoverRoutine()); // 시작할거에요 -> 회복 코루틴을
|
||
}
|
||
|
||
private IEnumerator RecoverRoutine() // 코루틴을 정의할거에요 -> 공격 후 휴식을
|
||
{
|
||
isAttacking = false; // 해제할거에요 -> 공격 상태를
|
||
isResting = true; // 설정할거에요 -> 휴식 상태로
|
||
if (animator != null) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
||
yield return new WaitForSeconds(patternInterval * 0.4f); // 기다릴거에요 -> 짧은 휴식 (간격의 40%)
|
||
isResting = false; // 해제할거에요 -> 휴식 상태를
|
||
_isPerformingAction = false; // 해제할거에요 -> 행동 상태를
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 이동 / 회전 유틸
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void AttachBallToHand() // 함수를 선언할거에요 -> 쇠공을 handHolder에 오프셋 적용해서 붙이는
|
||
{
|
||
if (ironBall == null || handHolder == null) return; // 중단할거에요 -> 없으면
|
||
|
||
Rigidbody rb = ironBall.GetComponent<Rigidbody>(); // 가져올거에요 -> Rigidbody를
|
||
if (rb != null) // 조건이 맞으면 실행할거에요 -> Rigidbody 있으면
|
||
{
|
||
rb.isKinematic = true; // 고정할거에요 -> 물리를
|
||
rb.velocity = Vector3.zero; // 초기화할거에요 -> 속도를
|
||
rb.angularVelocity = Vector3.zero; // 초기화할거에요 -> 회전 속도를
|
||
}
|
||
|
||
ironBall.transform.SetParent(handHolder); // 붙일거에요 -> 손에
|
||
ironBall.transform.localPosition = ballHoldOffset; // 설정할거에요 -> 오프셋 위치로 (Inspector에서 조정)
|
||
ironBall.transform.localRotation = Quaternion.identity; // 초기화할거에요 -> 로컬 회전을
|
||
}
|
||
|
||
private void LookAt(Vector3 targetPos) // 함수를 선언할거에요 -> 목표 방향으로 회전하는
|
||
{
|
||
Vector3 dir = targetPos - transform.position; // 계산할거에요 -> 방향 벡터를
|
||
dir.y = 0f; // 무시할거에요 -> 높이 차이를
|
||
if (dir.sqrMagnitude > 0.001f) // 조건이 맞으면 실행할거에요 -> 방향이 있으면
|
||
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * 8f); // 회전할거에요 -> 부드럽게
|
||
}
|
||
|
||
private void UpdateMoveAnim() // 함수를 선언할거에요 -> 이동 애니메이션을 갱신하는
|
||
{
|
||
if (animator == null || _isPerformingAction || isAttacking || isHit || isDead) return; // 중단할거에요 -> 행동 불가 상태면
|
||
bool moving = agent != null && agent.velocity.magnitude > 0.1f; // 판단할거에요 -> 이동 중인지를
|
||
var state = animator.GetCurrentAnimatorStateInfo(0); // 가져올거에요 -> 현재 애니 상태를
|
||
if (moving && !state.IsName(anim_Walk)) animator.Play(anim_Walk); // 재생할거에요 -> 걷기 애니를
|
||
if (!moving && !state.IsName(anim_Idle) && !state.IsName(anim_Roar)) animator.Play(anim_Idle); // 재생할거에요 -> 대기 애니를
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 사망 / 피격 오버라이드
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
protected override void OnStartHit() { } // 비워둘거에요 -> 보스 슈퍼아머 (피격에 행동 안 끊김)
|
||
|
||
protected override void OnDie() // 함수를 정의할거에요 -> 사망 처리를
|
||
{
|
||
_isBattleStarted = _isPerformingAction = _isDashing = false; // 해제할거에요 -> 모든 상태를
|
||
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 끌거에요 -> 체력바를
|
||
if (ObsessionSystem.instance != null) ObsessionSystem.instance.AddRunXP(100); // 지급할거에요 -> 경험치를
|
||
else Debug.LogError("[Boss] ObsessionSystem 인스턴스 없음!"); // 에러를 찍을거에요
|
||
base.OnDie(); // 실행할거에요 -> 부모 사망 처리를
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// Inspector 패턴 테스트 버튼
|
||
// 플레이 중 Inspector에서 bool을 true로 체크 → 즉시 해당 패턴 실행
|
||
// 실행 후 자동으로 false로 리셋됩니다
|
||
// ══════════════════════════════════════════════════════════
|
||
|
||
[Header("=== 패턴 테스트 (플레이 중 체크) ===")]
|
||
[Tooltip("체크 시 쇠공 던지기 패턴 즉시 실행")]
|
||
[SerializeField] private bool _testThrow = false; // 변수를 선언할거에요 -> 던지기 테스트 트리거를
|
||
[Tooltip("체크 시 내려찍기 패턴 즉시 실행")]
|
||
[SerializeField] private bool _testSmash = false; // 변수를 선언할거에요 -> 찍기 테스트 트리거를
|
||
[Tooltip("체크 시 휩쓸기 패턴 즉시 실행")]
|
||
[SerializeField] private bool _testSweep = false; // 변수를 선언할거에요 -> 휩쓸기 테스트 트리거를
|
||
[Tooltip("체크 시 돌진 패턴 즉시 실행")]
|
||
[SerializeField] private bool _testDash = false; // 변수를 선언할거에요 -> 돌진 테스트 트리거를
|
||
[Tooltip("체크 시 연속 스매쉬 패턴 즉시 실행 (Phase2 패턴)")]
|
||
[SerializeField] private bool _testComboSmash = false; // 변수를 선언할거에요 -> 연속찍기 테스트 트리거를
|
||
[Tooltip("체크 시 돌진+찍기 콤보 즉시 실행 (Phase2 패턴)")]
|
||
[SerializeField] private bool _testDashSmash = false; // 변수를 선언할거에요 -> 돌진찍기 테스트 트리거를
|
||
[Tooltip("체크 시 Phase2 즉시 진입 (포효 연출 포함)")]
|
||
[SerializeField] private bool _testPhase2 = false; // 변수를 선언할거에요 -> Phase2 강제 진입 트리거를
|
||
|
||
private void PollTestButtons() // 함수를 선언할거에요 -> 매 프레임 Inspector 테스트 버튼을 감지하는
|
||
{
|
||
if (_isPerformingAction) return; // 중단할거에요 -> 패턴 실행 중이면 무시
|
||
|
||
if (_testThrow) { _testThrow = false; ForcePattern(BossPattern.Throw); return; } // 실행할거에요 -> 던지기 테스트를
|
||
if (_testSmash) { _testSmash = false; ForcePattern(BossPattern.Smash); return; } // 실행할거에요 -> 찍기 테스트를
|
||
if (_testSweep) { _testSweep = false; ForcePattern(BossPattern.Sweep); return; } // 실행할거에요 -> 휩쓸기 테스트를
|
||
if (_testDash) { _testDash = false; ForcePattern(BossPattern.Dash); return; } // 실행할거에요 -> 돌진 테스트를
|
||
if (_testComboSmash) { _testComboSmash = false; ForcePattern(BossPattern.ComboSmash); return; } // 실행할거에요 -> 연속찍기 테스트를
|
||
if (_testDashSmash) { _testDashSmash = false; ForcePattern(BossPattern.DashSmash); return; } // 실행할거에요 -> 돌진찍기 테스트를
|
||
|
||
if (_testPhase2) // 조건이 맞으면 실행할거에요 -> Phase2 테스트이면
|
||
{
|
||
_testPhase2 = false; // 초기화할거에요 -> 트리거를
|
||
_phase2Triggered = false; // 초기화할거에요 -> 이미 발동됨 플래그를 (재발동 허용)
|
||
StartCoroutine(EnterPhase2()); // 시작할거에요 -> Phase2 진입 코루틴을
|
||
}
|
||
}
|
||
|
||
private void ForcePattern(BossPattern pattern) // 함수를 선언할거에요 -> 패턴을 즉시 강제 실행하는
|
||
{
|
||
// 전투 미시작 상태에서도 테스트 가능하도록 타겟 자동 탐색
|
||
if (_target == null) // 조건이 맞으면 실행할거에요 -> 타겟 없으면
|
||
{
|
||
GameObject p = GameObject.FindWithTag("Player"); // 찾을거에요 -> 플레이어를
|
||
if (p != null) _target = playerTransform = p.transform; // 설정할거에요 -> 타겟을
|
||
}
|
||
|
||
Debug.Log($"[BossTest] 강제 패턴 실행: {pattern}"); // 로그를 찍을거에요
|
||
|
||
switch (pattern) // 분기할거에요 -> 패턴에 따라
|
||
{
|
||
case BossPattern.Throw: StartCoroutine(Pattern_Throw()); break;
|
||
case BossPattern.Smash: StartCoroutine(Pattern_Smash()); break;
|
||
case BossPattern.Sweep: StartCoroutine(Pattern_Sweep()); break;
|
||
case BossPattern.Dash: StartCoroutine(Pattern_Dash()); break;
|
||
case BossPattern.ComboSmash: StartCoroutine(Pattern_ComboSmash()); break;
|
||
case BossPattern.DashSmash: StartCoroutine(Pattern_DashSmash()); break;
|
||
}
|
||
}
|
||
|
||
|
||
// ──────────────────────────────────────────────────────────
|
||
// 디버그 기즈모
|
||
// ──────────────────────────────────────────────────────────
|
||
|
||
private void OnDrawGizmosSelected() // 함수를 실행할거에요 -> 씬 뷰 선택 시 판정 범위 시각화를
|
||
{
|
||
Vector3 smashCenter = transform.position + Vector3.up * smashHeightOffset; // 계산할거에요 -> 찍기 판정 중심을
|
||
Vector3 sweepCenter = transform.position + Vector3.up * sweepHeightOffset; // 계산할거에요 -> 휩쓸기 판정 중심을
|
||
|
||
Gizmos.color = Color.red; Gizmos.DrawWireSphere(smashCenter, smashRadius); // 그릴거에요 -> 찍기 범위를
|
||
Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(sweepCenter, sweepRadius); // 그릴거에요 -> 휩쓸기 범위를
|
||
Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(transform.position, meleeRange); // 그릴거에요 -> 근접 사거리를
|
||
if (ironBall != null) { Gizmos.color = Color.magenta; Gizmos.DrawWireSphere(ironBall.transform.position, ballLandRadius); } // 그릴거에요 -> 쇠공 착지 범위를
|
||
}
|
||
} |