2026-02-12 15:23:25 +00:00
|
|
|
|
using UnityEngine; // 유니티 기본 기능을 불러올거에요 -> UnityEngine을
|
|
|
|
|
|
using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Collections를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public class NorcielBoss : MonsterClass // 클래스를 선언할거에요 -> MonsterClass를 상속받는 NorcielBoss를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
[Header("--- 🧠 두뇌 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🧠 두뇌 연결 ---을
|
|
|
|
|
|
[SerializeField] private BossCounterSystem counterSystem; // 변수를 선언할거에요 -> 보스의 패턴을 결정할 counterSystem을
|
|
|
|
|
|
|
|
|
|
|
|
[Header("--- ⚔️ 패턴 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- ⚔️ 패턴 설정 ---을
|
|
|
|
|
|
[SerializeField] private float patternInterval = 3f; // 변수를 선언할거에요 -> 공격 후 다음 공격까지 대기 시간을 3초로 지정하는 patternInterval을
|
|
|
|
|
|
[SerializeField] private float attackRange = 3f; // 변수를 선언할거에요 -> 이 거리 안에 플레이어가 들어오면 공격하는 attackRange를 3으로
|
|
|
|
|
|
|
|
|
|
|
|
[Header("--- 🎱 무기(쇠공) 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🎱 무기 설정 ---을
|
|
|
|
|
|
[SerializeField] private GameObject ironBall; // 변수를 선언할거에요 -> 던지고 주울 무기 오브젝트인 ironBall을
|
|
|
|
|
|
[SerializeField] private Transform handHolder; // 변수를 선언할거에요 -> 무기를 잡을 손의 위치인 handHolder를
|
|
|
|
|
|
|
|
|
|
|
|
[Header("--- 📊 UI 연결 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 📊 UI 연결 ---을
|
|
|
|
|
|
[SerializeField] private GameObject bossHealthBar; // 변수를 선언할거에요 -> 보스의 체력 UI를 담을 bossHealthBar를
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ [핵심 수정] 애니메이션 이름을 인스펙터에서 맞출 수 있게 변수로 분리
|
|
|
|
|
|
[Header("--- 🎬 애니메이션 이름 설정 (Animator와 일치시킬 것!) ---")]
|
|
|
|
|
|
[SerializeField] private string anim_Roar = "Roar"; // 변수를 선언할거에요 -> 포효 애니메이션 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Idle = "Monster_Idle"; // 변수를 선언할거에요 -> 대기 애니메이션 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Walk = "Monster_Walk"; // 변수를 선언할거에요 -> 걷기 애니메이션 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Pickup = "Skill_Pickup"; // 변수를 선언할거에요 -> 무기 줍기 애니메이션 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Throw = "Attack_Throw"; // 변수를 선언할거에요 -> 던지기 애니메이션 이름을
|
|
|
|
|
|
|
|
|
|
|
|
[Header("--- 🎬 스킬 애니메이션 이름 ---")]
|
|
|
|
|
|
[SerializeField] private string anim_DashReady = "Skill_Dash_Ready"; // 변수를 선언할거에요 -> 돌진 준비 애니메이션 이름을
|
|
|
|
|
|
[SerializeField] private string anim_DashGo = "Skill_Dash_Go"; // 변수를 선언할거에요 -> 돌진 출발 애니메이션 이름을
|
|
|
|
|
|
[SerializeField] private string anim_SmashCharge = "Skill_Smash_Charge"; // 변수를 선언할거에요 -> 내려찍기 기모으기 이름을
|
|
|
|
|
|
[SerializeField] private string anim_SmashImpact = "Skill_Smash_Impact"; // 변수를 선언할거에요 -> 내려찍기 타격 이름을
|
|
|
|
|
|
[SerializeField] private string anim_Sweep = "Skill_Sweep"; // 변수를 선언할거에요 -> 휩쓸기 애니메이션 이름을
|
|
|
|
|
|
|
|
|
|
|
|
[Header("--- 🔍 디버그 (읽기 전용) ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 🔍 디버그 ---를
|
|
|
|
|
|
[SerializeField] private string debugState = "대기 중"; // 변수를 선언할거에요 -> 현재 보스 행동을 문장으로 보여줄 debugState를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// --- 내부 변수들 ---
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private float _timer; // 변수를 선언할거에요 -> 공격 쿨타임을 계산할 내부 타이머 _timer를
|
|
|
|
|
|
private Rigidbody rb; // 변수를 선언할거에요 -> 보스의 물리 엔진을 제어할 rb를
|
|
|
|
|
|
private Rigidbody ballRb; // 변수를 선언할거에요 -> 쇠공의 물리 엔진을 제어할 ballRb를
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private bool isBattleStarted = false; // 변수를 초기화할거에요 -> 전투 시작 상태를 거짓(false)으로
|
|
|
|
|
|
private bool isWeaponless = false; // 변수를 초기화할거에요 -> 맨손 상태를 거짓(false)으로
|
|
|
|
|
|
private bool isPerformingAction = false; // 변수를 초기화할거에요 -> 연출 진행 상태를 거짓(false)으로
|
|
|
|
|
|
private Transform target; // 변수를 선언할거에요 -> 보스가 쫓아갈 대상의 위치인 target을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
protected override void Awake() // 함수를 실행할거에요 -> 스크립트가 켜질 때 가장 먼저 호출되는 Awake를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
base.Awake(); // 부모 클래스의 함수를 먼저 실행할거에요 -> MonsterClass의 Awake 로직을
|
|
|
|
|
|
rb = GetComponent<Rigidbody>(); // 컴포넌트를 가져와서 저장할거에요 -> 보스 자신의 Rigidbody를 rb에
|
|
|
|
|
|
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>(); // 조건이 맞으면 가져올거에요 -> ironBall이 있다면 그것의 Rigidbody를 ballRb에
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private void Start() // 함수를 실행할거에요 -> 첫 프레임이 시작될 때 호출되는 Start를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
StartBossBattle(); // 함수를 실행할거에요 -> Roar 연출 후 전투를 시작하는 StartBossBattle을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
protected override void Init() // 함수를 덮어씌워 실행할거에요 -> 몬스터 초기화를 담당하는 Init을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
base.Init(); // 부모 초기화 호출
|
|
|
|
|
|
_timer = 0f; // 값을 넣을거에요 -> 전투 시작하자마자 첫 공격이 가능하게 타이머를 0으로
|
|
|
|
|
|
isBattleStarted = false; // 상태를 바꿀거에요 -> 전투 시작 상태를 거짓(false)으로
|
|
|
|
|
|
isWeaponless = false; // 상태를 바꿀거에요 -> 맨손 상태를 거짓(false)으로
|
|
|
|
|
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 상태를 거짓(false)으로
|
|
|
|
|
|
|
|
|
|
|
|
IsAggroed = true; // 상태를 바꿀거에요 -> 부모 클래스의 영구 어그로 상태를 참(true)으로
|
|
|
|
|
|
mobRenderer = null; // 값을 지울거에요 -> 화면 밖 최적화를 끄기 위해 렌더러를 null로
|
|
|
|
|
|
optimizationDistance = 9999f; // 값을 바꿀거에요 -> 거리가 멀어져도 멈추지 않게 최적화 거리를 9999로
|
|
|
|
|
|
|
|
|
|
|
|
GameObject playerObj = GameObject.FindWithTag("Player"); // 오브젝트를 찾을거에요 -> Player 태그가 붙은 녀석을
|
|
|
|
|
|
if (playerObj != null) // 조건이 맞으면 실행할거에요 -> 태그로 찾은 플레이어가 존재한다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
target = playerObj.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
|
|
|
|
|
|
playerTransform = playerObj.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
|
|
|
|
|
|
}
|
|
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> 태그로 찾지 못했다면
|
|
|
|
|
|
{
|
|
|
|
|
|
var playerScript = FindObjectOfType<PlayerMovement>(); // 컴포넌트를 찾을거에요 -> PlayerMovement 스크립트를 가진 오브젝트를
|
|
|
|
|
|
if (playerScript != null) // 조건이 맞으면 실행할거에요 -> 스크립트로 플레이어를 찾았다면
|
|
|
|
|
|
{
|
|
|
|
|
|
target = playerScript.transform; // 값을 넣을거에요 -> 플레이어의 위치를 target에
|
|
|
|
|
|
playerTransform = playerScript.transform; // 값을 넣을거에요 -> 부모의 추적 변수에도 플레이어의 위치를
|
|
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (target == null) // 조건이 맞으면 실행할거에요 -> 어떤 방법으로도 플레이어를 못 찾았다면
|
|
|
|
|
|
Debug.LogError("❌ [Boss] 플레이어를 찾지 못했습니다! Player 태그 또는 PlayerMovement 확인!"); // 에러를 출력할거에요 -> 플레이어 없음 경고 메시지를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
currentHP = maxHP; // 체력을 채울거에요 -> 현재 체력을 최대 체력으로
|
|
|
|
|
|
StartCoroutine(SafeInitNavMesh()); // 코루틴을 실행할거에요 -> 길찾기를 안전하게 켜는 SafeInitNavMesh를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
|
|
|
|
|
|
if (ballRb != null) ballRb.isKinematic = true; // 조건이 맞으면 실행할거에요 -> 쇠공 물리가 있다면 중력 연산을 끄기를
|
|
|
|
|
|
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 애니메이터가 있다면 대기 모션을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private IEnumerator SafeInitNavMesh() // 코루틴 함수를 선언할거에요 -> 길찾기를 안전하게 초기화하는 SafeInitNavMesh를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
yield return null; // 기다릴거에요 -> 다음 프레임이 될 때까지 한 턴을
|
|
|
|
|
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> NavMeshAgent가 존재한다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
|
|
|
|
|
|
while (!agent.isOnNavMesh && retry < 5) // 반복할거에요 -> 바닥에 없고 시도가 5번 미만인 동안
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
|
|
|
|
|
|
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (agent.isOnNavMesh) agent.isStopped = true; // 조건이 맞으면 실행할거에요 -> 바닥에 닿았다면 이동을 정지로
|
|
|
|
|
|
agent.enabled = false; // 기능을 끌거에요 -> 전투 시작 전까지 NavMeshAgent를 비활성화로
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
protected override void Update() // 함수를 실행할거에요 -> 매 프레임마다 호출되는 Update를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
base.Update(); // 부모 클래스의 Update도 호출 (최적화 로직 등)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (!isDead) // 조건이 맞으면 실행할거에요 -> 보스가 살아있다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
ExecuteAILogic(); // 함수를 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 🧠 AI 핵심 로직
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
protected override void ExecuteAILogic() // 함수를 덮어씌워 실행할거에요 -> 보스 AI 두뇌인 ExecuteAILogic을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (isPerformingAction || !isBattleStarted || target == null) return; // 조건이 맞으면 중단할거에요 -> 연출중, 전투전, 타겟없음 중 하나라도 해당되면
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (isWeaponless) // 조건이 맞으면 실행할거에요 -> 쇠공을 던져서 맨손이라면
|
2026-02-11 07:21:58 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
RetrieveWeaponLogic(); // 함수를 실행할거에요 -> 쇠공을 주우러 가는 로직을
|
|
|
|
|
|
return; // 중단할거에요 -> 무기 줍기가 우선이므로 아래 로직을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (isAttacking || isHit || isResting) return; // 조건이 맞으면 중단할거에요 -> 다른 행동 중이라면 AI 판단을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (_timer > 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 남아있다면
|
2026-02-11 07:21:58 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
_timer -= Time.deltaTime; // 값을 뺄거에요 -> 쿨타임 타이머에서 지난 프레임 시간만큼을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
float distToPlayer = Vector3.Distance(transform.position, target.position); // 거리를 계산할거에요 -> 보스와 플레이어 사이의 거리를
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (agent == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 길찾기가 불가능한 상태라면
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (distToPlayer > attackRange) // 조건이 맞으면 실행할거에요 -> 플레이어가 공격 사거리 밖이라면
|
2026-02-11 07:21:58 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
|
|
|
|
|
|
agent.SetDestination(target.position); // 명령을 내릴거에요 -> 길찾기 목표를 플레이어 위치로
|
|
|
|
|
|
LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
|
|
|
|
|
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걷기 애니메이션을 맞추는 UpdateMovementAnimation을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
}
|
2026-02-12 15:23:25 +00:00
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> 플레이어가 사거리 안에 있다면
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
|
|
|
|
|
agent.velocity = Vector3.zero; // 값을 넣을거에요 -> 이동 속도를 완전한 0으로
|
|
|
|
|
|
LookAtTarget(target.position); // 함수를 실행할거에요 -> 플레이어를 바라보는 LookAtTarget을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (animator != null) // 조건이 맞으면 실행할거에요 -> 애니메이터가 존재한다면
|
|
|
|
|
|
{
|
|
|
|
|
|
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
|
|
|
|
|
|
if (state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 걷기 모션이 재생 중이라면
|
|
|
|
|
|
{
|
|
|
|
|
|
animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (_timer <= 0) // 조건이 맞으면 실행할거에요 -> 공격 쿨타임이 0 이하라면
|
|
|
|
|
|
{
|
|
|
|
|
|
_timer = patternInterval; // 값을 넣을거에요 -> 쿨타임을 다시 패턴 대기 시간으로
|
|
|
|
|
|
Debug.Log($"⏰ [Boss] 사거리 내 공격! 거리={distToPlayer:F1}m"); // 로그를 출력할거에요 -> 공격 시점과 거리 정보를
|
|
|
|
|
|
DecideAttack(); // 함수를 실행할거에요 -> 어떤 공격을 할지 고르는 DecideAttack을
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 07:21:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-11 13:47:53 +00:00
|
|
|
|
// 🎬 전투 입장
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public void StartBossBattle() // 함수를 선언할거에요 -> 외부에서 전투를 시작시킬 수 있는 StartBossBattle을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (isBattleStarted) return; // 조건이 맞으면 중단할거에요 -> 이미 전투가 시작되었다면 중복 실행을
|
|
|
|
|
|
StartCoroutine(BattleStartRoutine()); // 코루틴을 실행할거에요 -> Roar 연출 후 전투를 시작하는 BattleStartRoutine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private IEnumerator BattleStartRoutine() // 코루틴 함수를 선언할거에요 -> Roar 연출과 전투 시작을 담당하는 BattleStartRoutine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
Debug.Log("🔥 [Boss] Roar 시작!"); // 로그를 출력할거에요 -> Roar가 시작되었다는 메시지를
|
|
|
|
|
|
isPerformingAction = true; // 상태를 바꿀거에요 -> 연출 중 플래그를 참(true)으로
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(true); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에 켜기를
|
|
|
|
|
|
if (counterSystem != null) counterSystem.InitializeBattle(); // 조건이 맞으면 실행할거에요 -> 카운터 시스템이 있다면 전투 초기화를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (animator != null) animator.Play(anim_Roar, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar 애니메이션을 처음부터 재생을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> Roar 애니메이션이 끝날 2초의 시간을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (animator != null) animator.Play(anim_Idle, 0, 0f); // 조건이 맞으면 실행할거에요 -> Roar가 끝났으니 대기 모션으로 전환을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 중 플래그를 거짓(false)으로
|
|
|
|
|
|
isBattleStarted = true; // 상태를 바꿀거에요 -> 전투 시작 플래그를 참(true)으로
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
Debug.Log("⚔️ [Boss] Roar 종료 → 전투 루프 시작!"); // 로그를 출력할거에요 -> 전투 루프가 시작되었다는 메시지를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (agent != null) // 조건이 맞으면 실행할거에요 -> 길찾기 에이전트가 존재한다면
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
agent.enabled = true; // 기능을 켤거에요 -> 길찾기 에이전트를 활성화(true)로
|
|
|
|
|
|
int retry = 0; // 변수를 선언할거에요 -> 재시도 횟수를 셀 retry를 0으로
|
|
|
|
|
|
while (!agent.isOnNavMesh && retry < 10) // 반복할거에요 -> NavMesh에 안 닿았고 10번 미만인 동안
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
retry++; // 값을 증가시킬거에요 -> 재시도 횟수를 1만큼
|
|
|
|
|
|
yield return new WaitForSeconds(0.1f); // 기다릴거에요 -> 0.1초의 시간을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (agent.isOnNavMesh) // 조건이 맞으면 실행할거에요 -> NavMesh 위에 올라갔다면
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 추격을 시작하라고
|
|
|
|
|
|
Debug.Log("✅ [Boss] NavMesh 연결 완료, 추격 시작"); // 로그를 출력할거에요 -> NavMesh 연결 성공 메시지를
|
|
|
|
|
|
}
|
|
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> NavMesh에 못 올라갔다면
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.LogError("❌ [Boss] NavMesh 연결 실패! 보스 위치를 확인하세요!"); // 에러를 출력할거에요 -> 연결 실패 경고를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
2026-02-11 07:21:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 🚶 이동 애니메이션 & 타겟팅
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private void UpdateMovementAnimation() // 함수를 선언할거에요 -> 이동 상태에 맞는 애니메이션을 재생하는 UpdateMovementAnimation을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (animator == null || isPerformingAction || isAttacking || isHit || isDead) return; // 조건이 맞으면 중단할거에요 -> 애니메이션을 바꿀 수 없는 상태라면
|
|
|
|
|
|
|
|
|
|
|
|
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); // 변수를 선언할거에요 -> 현재 재생 중인 애니메이션 상태를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (agent != null && agent.velocity.magnitude > 0.1f) // 조건이 맞으면 실행할거에요 -> 현재 이동 속도가 0.1보다 크다면
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (!state.IsName(anim_Walk)) // 조건이 맞으면 실행할거에요 -> 아직 걷기 모션이 아니라면
|
|
|
|
|
|
animator.Play(anim_Walk); // 애니메이션을 바꿀거에요 -> 걷기 모션으로
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
2026-02-12 15:23:25 +00:00
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> 거의 멈춰있다면
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!state.IsName(anim_Idle) && !state.IsName(anim_Roar)) // 조건이 맞으면 실행할거에요 -> 대기나 포효 모션이 아니라면
|
|
|
|
|
|
animator.Play(anim_Idle); // 애니메이션을 바꿀거에요 -> 대기 모션으로
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private void LookAtTarget(Vector3 targetPos) // 함수를 선언할거에요 -> 타겟을 부드럽게 쳐다보는 LookAtTarget을
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 dir = targetPos - transform.position; // 벡터를 계산할거에요 -> 내 위치에서 타겟까지의 방향을
|
|
|
|
|
|
dir.y = 0; // 값을 바꿀거에요 -> 위아래로 고개가 꺾이지 않게 y를 0으로
|
|
|
|
|
|
if (dir != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 0이 아니라면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
transform.rotation = Quaternion.Slerp( // 회전시킬거에요 -> 현재 회전에서 타겟 방향으로 부드럽게
|
|
|
|
|
|
transform.rotation, // 시작값으로 사용할거에요 -> 현재 회전값을
|
|
|
|
|
|
Quaternion.LookRotation(dir), // 목표값으로 사용할거에요 -> 타겟을 향하는 회전값을
|
|
|
|
|
|
Time.deltaTime * 5f // 속도를 지정할거에요 -> 프레임 시간의 5배 속도로
|
|
|
|
|
|
);
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 🧠 공격 판단
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private void DecideAttack() // 함수를 선언할거에요 -> 어떤 공격 패턴을 쓸지 고르는 DecideAttack을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (target != null) LookAtTarget(target.position); // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 그쪽을 바라보기를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal"; // 변수를 선언할거에요 -> 카운터 시스템에서 받은 패턴 이름을 patternName에
|
|
|
|
|
|
Debug.Log($"🧠 [Boss] 패턴 선택: {patternName}"); // 로그를 출력할거에요 -> 선택된 패턴 이름을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
switch (patternName) // 분기할거에요 -> 받아온 패턴 이름에 따라서
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
case "DashAttack": StartCoroutine(Pattern_DashAttack()); break; // 일치하면 실행할거에요 -> 돌진 공격을
|
|
|
|
|
|
case "Smash": StartCoroutine(Pattern_SmashAttack()); break; // 일치하면 실행할거에요 -> 내려찍기 공격을
|
|
|
|
|
|
case "ShieldWall": StartCoroutine(Pattern_ShieldWall()); break; // 일치하면 실행할거에요 -> 휩쓸기 공격을
|
|
|
|
|
|
default: StartCoroutine(Pattern_ThrowBall()); break; // 맞는게 없으면 실행할거에요 -> 공 던지기 공격을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
2026-02-12 15:23:25 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
// 🏃 무기 회수
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private void RetrieveWeaponLogic() // 함수를 선언할거에요 -> 던진 쇠공을 주우러 가는 RetrieveWeaponLogic을
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ironBall == null || !agent.enabled || !agent.isOnNavMesh) return; // 조건이 맞으면 중단할거에요 -> 쇠공이 없거나 길찾기가 불가능하다면
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
agent.SetDestination(ironBall.transform.position); // 명령을 내릴거에요 -> 길찾기 목표를 쇠공의 위치로
|
|
|
|
|
|
agent.isStopped = false; // 명령을 내릴거에요 -> 이동 정지를 풀고 걸어가라고
|
|
|
|
|
|
UpdateMovementAnimation(); // 함수를 실행할거에요 -> 걸어갈 때 이동 애니메이션이 나오도록
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (Vector3.Distance(transform.position, ironBall.transform.position) <= 3.0f && !isAttacking) // 조건이 맞으면 실행할거에요 -> 쇠공과 3m 이내이고 공격 중이 아니라면
|
|
|
|
|
|
StartCoroutine(PickUpBallRoutine()); // 코루틴을 실행할거에요 -> 바닥의 쇠공을 줍는 PickUpBallRoutine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private IEnumerator PickUpBallRoutine() // 코루틴 함수를 선언할거에요 -> 쇠공 줍기 행동인 PickUpBallRoutine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
OnAttackStart(); // 상태를 켤거에요 -> 다른 행동과 겹치지 않게 공격 플래그를
|
|
|
|
|
|
if (agent != null && agent.isOnNavMesh) { agent.isStopped = true; agent.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 줍기 위해 이동을 멈추고 속도를 0으로
|
|
|
|
|
|
if (animator != null) animator.Play(anim_Pickup); // 조건이 맞으면 실행할거에요 -> 줍는 애니메이션을
|
|
|
|
|
|
yield return new WaitForSeconds(0.8f); // 기다릴거에요 -> 손이 바닥에 닿을 0.8초를
|
|
|
|
|
|
|
|
|
|
|
|
ironBall.transform.SetParent(handHolder); // 부모를 바꿀거에요 -> 쇠공을 보스의 손 밑으로
|
|
|
|
|
|
ironBall.transform.localPosition = Vector3.zero; // 위치를 바꿀거에요 -> 쇠공의 위치를 손의 정중앙으로
|
|
|
|
|
|
ironBall.transform.localRotation = Quaternion.identity; // 회전을 바꿀거에요 -> 쇠공의 회전을 기본값으로
|
|
|
|
|
|
if (ballRb != null) { ballRb.isKinematic = true; ballRb.velocity = Vector3.zero; } // 조건이 맞으면 실행할거에요 -> 쇠공의 물리를 끄고 속도를 0으로
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 일어서는 애니메이션이 끝날 1초를
|
|
|
|
|
|
isWeaponless = false; // 상태를 바꿀거에요 -> 무기를 다시 쥐었으므로 맨손 상태를 거짓으로
|
|
|
|
|
|
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 처리를
|
|
|
|
|
|
if (agent != null && agent.isOnNavMesh) agent.isStopped = false; // 조건이 맞으면 실행할거에요 -> 다시 추격을 위해 이동 정지 해제를
|
|
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
// ✅ 공격 시작/종료 처리 (핵심)
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public override void OnAttackStart() // 함수를 덮어씌워 실행할거에요 -> 공격 시작 처리를 하는 OnAttackStart를
|
|
|
|
|
|
{
|
|
|
|
|
|
isAttacking = true; // 상태를 바꿀거에요 -> 공격 중임을 참으로
|
|
|
|
|
|
isPerformingAction = true; // 상태를 바꿀거에요 -> 다른 행동 불가 상태로
|
|
|
|
|
|
if (agent != null) agent.isStopped = true; // 명령을 내릴거에요 -> 이동을 멈추라고
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
public override void OnAttackEnd() // 함수를 덮어씌워 실행할거에요 -> 공격 종료 처리를 하는 OnAttackEnd를
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
StartCoroutine(RecoverRoutine()); // 코루틴을 시작할거에요 -> 후딜레이(회복) 처리를 위한 RecoverRoutine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private IEnumerator RecoverRoutine() // 코루틴 함수를 선언할거에요 -> 공격 후 대기 시간을 갖는 RecoverRoutine을
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
isAttacking = false; // 상태를 바꿀거에요 -> 공격이 끝났으므로 공격 중 상태를 거짓으로
|
|
|
|
|
|
isResting = true; // 상태를 바꿀거에요 -> 휴식 중 상태를 참으로 (이때는 추격 안 함)
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (animator != null) animator.Play(anim_Idle); // 조건이 맞으면 실행할거에요 -> 대기 모션으로 전환을
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 딜레이 시간 (여기서 patternInterval 만큼 멍때림)
|
|
|
|
|
|
yield return new WaitForSeconds(patternInterval); // 기다릴거에요 -> 설정된 패턴 간격 시간만큼
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
isResting = false; // 상태를 바꿀거에요 -> 휴식이 끝났으므로 거짓으로
|
|
|
|
|
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 이제 다시 다른 행동이 가능하도록 거짓으로
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// ⚔️ 공격 패턴들
|
2026-02-11 13:47:53 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 패턴 1: 돌진 공격
|
|
|
|
|
|
private IEnumerator Pattern_DashAttack() // 코루틴 함수를 선언할거에요 -> 돌진 패턴인 Pattern_DashAttack을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
|
|
|
|
|
if (agent != null) agent.enabled = false; // 조건이 맞으면 실행할거에요 -> 돌진 시 물리력을 위해 길찾기를 끄기로
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (animator != null) animator.Play(anim_DashReady, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 준비 모션을
|
|
|
|
|
|
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 준비 포즈를 취할 0.5초를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
if (rb != null) { rb.isKinematic = false; rb.velocity = transform.forward * 20f; } // 조건이 맞으면 실행할거에요 -> 물리를 켜고 앞으로 속도 20으로 돌진을
|
|
|
|
|
|
if (animator != null) animator.Play(anim_DashGo, 0, 0f); // 조건이 맞으면 실행할거에요 -> 돌진 애니메이션을
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 돌진이 끝날 1초를
|
|
|
|
|
|
|
|
|
|
|
|
if (rb != null) { rb.velocity = Vector3.zero; rb.isKinematic = true; } // 조건이 맞으면 실행할거에요 -> 속도를 0으로 멈추고 물리를 끄기로
|
|
|
|
|
|
if (agent != null && isBattleStarted) agent.enabled = true; // 조건이 맞으면 실행할거에요 -> 전투 중이면 길찾기를 다시 켜기로
|
|
|
|
|
|
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 패턴 2: 내려찍기
|
|
|
|
|
|
private IEnumerator Pattern_SmashAttack() // 코루틴 함수를 선언할거에요 -> 내려찍기 패턴인 Pattern_SmashAttack을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
|
|
|
|
|
if (animator != null) animator.Play(anim_SmashCharge, 0, 0f); // 조건이 맞으면 실행할거에요 -> 기를 모으는 모션을
|
|
|
|
|
|
yield return new WaitForSeconds(1.2f); // 기다릴거에요 -> 기를 모을 1.2초를
|
|
|
|
|
|
if (animator != null) animator.Play(anim_SmashImpact, 0, 0f); // 조건이 맞으면 실행할거에요 -> 내려찍는 모션을
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 자세를 가다듬을 1초를
|
|
|
|
|
|
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 패턴 3: 휩쓸기
|
|
|
|
|
|
private IEnumerator Pattern_ShieldWall() // 코루틴 함수를 선언할거에요 -> 휩쓸기 패턴인 Pattern_ShieldWall을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
|
|
|
|
|
if (animator != null) animator.Play(anim_Sweep, 0, 0f); // 조건이 맞으면 실행할거에요 -> 휩쓰는 모션을
|
|
|
|
|
|
yield return new WaitForSeconds(2.0f); // 기다릴거에요 -> 휘두르기가 끝날 2초를
|
|
|
|
|
|
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
|
|
|
|
|
}
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 패턴 4: 공 던지기
|
|
|
|
|
|
private IEnumerator Pattern_ThrowBall() // 코루틴 함수를 선언할거에요 -> 쇠공 던지기 패턴인 Pattern_ThrowBall을
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 상태를 켤거에요 -> 공격 플래그를
|
|
|
|
|
|
if (animator != null) animator.Play(anim_Throw, 0, 0f); // 조건이 맞으면 실행할거에요 -> 던지는 모션을
|
|
|
|
|
|
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 공을 던지기 직전인 0.5초를
|
|
|
|
|
|
|
|
|
|
|
|
if (ironBall != null && ballRb != null) // 조건이 맞으면 실행할거에요 -> 쇠공과 물리 엔진이 존재한다면
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
ironBall.transform.SetParent(null); // 부모를 바꿀거에요 -> 쇠공을 손에서 완전히 분리로
|
|
|
|
|
|
ballRb.isKinematic = false; // 기능을 켤거에요 -> 쇠공의 물리 연산을 활성화로
|
|
|
|
|
|
Vector3 dir = (target.position - transform.position).normalized; // 벡터를 계산할거에요 -> 플레이어를 향하는 방향을
|
|
|
|
|
|
ballRb.AddForce(dir * 20f + Vector3.up * 5f, ForceMode.Impulse); // 힘을 가할거에요 -> 앞으로 20, 위로 5의 포물선 힘을
|
|
|
|
|
|
ballRb.angularDrag = 5f; // 마찰을 줄거에요 -> 쇠공이 멀리 안 굴러가게 회전 마찰 5를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
2026-02-12 15:23:25 +00:00
|
|
|
|
|
|
|
|
|
|
isWeaponless = true; // 상태를 바꿀거에요 -> 던졌으므로 맨손 상태를 참으로
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f); // 기다릴거에요 -> 던지고 자세를 가다듬을 1초를
|
|
|
|
|
|
OnAttackEnd(); // 함수를 실행할거에요 -> 공격 종료 및 딜레이 처리를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 💥 피격 / 사망
|
2026-02-11 13:47:53 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
protected override void OnStartHit() // 함수를 덮어씌워 실행할거에요 -> 보스가 맞았을 때 호출되는 OnStartHit을
|
2026-02-11 13:47:53 +00:00
|
|
|
|
{
|
2026-02-12 15:23:25 +00:00
|
|
|
|
// 보스는 피격되어도 행동이 끊기지 않습니다 (슈퍼아머)
|
|
|
|
|
|
// 만약 끊기게 하려면 여기서 isPerformingAction = false;
|
|
|
|
|
|
}
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
protected override void OnDie() // 함수를 덮어씌워 실행할거에요 -> 보스 체력이 0이 되면 호출되는 OnDie를
|
|
|
|
|
|
{
|
|
|
|
|
|
isBattleStarted = false; // 상태를 바꿀거에요 -> 전투가 끝났으므로 전투 플래그를 거짓으로
|
|
|
|
|
|
isPerformingAction = false; // 상태를 바꿀거에요 -> 연출 플래그를 거짓으로
|
|
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(false); // 조건이 맞으면 실행할거에요 -> 체력바 UI가 있다면 화면에서 끄기를
|
|
|
|
|
|
GiveBossXP(); // 함수를 실행할거에요 -> 경험치를 주는 GiveBossXP를
|
|
|
|
|
|
base.OnDie(); // 부모 클래스의 사망 처리를 호출할거에요 -> 애니메이션, 시체 제거 등
|
|
|
|
|
|
}
|
2026-02-11 13:47:53 +00:00
|
|
|
|
|
2026-02-12 15:23:25 +00:00
|
|
|
|
private void GiveBossXP() // 함수를 선언할거에요 -> 보스 처치 보상을 주는 GiveBossXP를
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.Log("[BossController] 보스 처치 시도 중..."); // 로그를 출력할거에요 -> 보스 처치 시도 메시지를
|
|
|
|
|
|
if (ObsessionSystem.instance != null) // 조건이 맞으면 실행할거에요 -> 경험치 시스템이 존재한다면
|
|
|
|
|
|
ObsessionSystem.instance.AddRunXP(100); // 함수를 실행할거에요 -> 경험치 100을 주는 AddRunXP를
|
|
|
|
|
|
else // 조건이 틀리면 실행할거에요 -> 시스템을 못 찾았다면
|
|
|
|
|
|
Debug.LogError("[BossController] ObsessionSystem 인스턴스를 찾을 수 없습니다!"); // 에러를 출력할거에요 -> 시스템 누락 경고를
|
|
|
|
|
|
Debug.Log("<color=red>[BossController] 보스 처치 처리 완료!</color>"); // 로그를 출력할거에요 -> 빨간 글씨로 처치 완료 메시지를
|
2026-02-11 13:47:53 +00:00
|
|
|
|
}
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|