2026-02-10 15:29:22 +00:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using System.Collections;
|
|
|
|
|
|
|
|
|
|
|
|
public class NorcielBoss : MonsterClass
|
|
|
|
|
|
{
|
|
|
|
|
|
[Header("--- 🧠 두뇌 연결 ---")]
|
|
|
|
|
|
[SerializeField] private BossCounterSystem counterSystem;
|
|
|
|
|
|
|
|
|
|
|
|
[Header("--- ⚔️ 패턴 설정 ---")]
|
|
|
|
|
|
[SerializeField] private float patternInterval = 3f; // 공격 간격
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
[Header("--- 🎱 무기(쇠공) 설정 (필수!) ---")]
|
|
|
|
|
|
[SerializeField] private GameObject ironBall; // 씬에 있는 실제 쇠공 오브젝트
|
|
|
|
|
|
[SerializeField] private Transform handHolder; // 쇠공이 붙어있을 손의 위치 (RightHand 뼈)
|
|
|
|
|
|
|
2026-02-10 15:29:22 +00:00
|
|
|
|
[Header("--- 📊 UI 연결 ---")]
|
2026-02-11 07:21:58 +00:00
|
|
|
|
[SerializeField] private GameObject bossHealthBar; // 보스 체력바 UI
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// --- 내부 변수들 ---
|
2026-02-10 15:29:22 +00:00
|
|
|
|
private float _timer;
|
2026-02-11 07:21:58 +00:00
|
|
|
|
private Rigidbody rb; // 보스 본체의 리지드바디
|
|
|
|
|
|
private Rigidbody ballRb; // 쇠공의 리지드바디
|
|
|
|
|
|
|
|
|
|
|
|
private bool isBattleStarted = false; // 전투 시작 여부
|
|
|
|
|
|
private bool isWeaponless = false; // 현재 무기가 없는가?
|
|
|
|
|
|
private Transform target; // 플레이어 타겟
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
protected override void Awake()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.Awake();
|
|
|
|
|
|
rb = GetComponent<Rigidbody>();
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
|
|
|
|
|
// 쇠공에서 리지드바디 가져오기
|
|
|
|
|
|
if (ironBall != null) ballRb = ironBall.GetComponent<Rigidbody>();
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void Init()
|
|
|
|
|
|
{
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 1. 변수 초기화
|
2026-02-10 15:29:22 +00:00
|
|
|
|
_timer = patternInterval;
|
2026-02-11 07:21:58 +00:00
|
|
|
|
isBattleStarted = false;
|
|
|
|
|
|
isWeaponless = false;
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 2. 플레이어 찾기
|
2026-02-10 15:29:22 +00:00
|
|
|
|
GameObject playerObj = GameObject.FindWithTag("Player");
|
2026-02-11 07:21:58 +00:00
|
|
|
|
if (playerObj != null) target = playerObj.transform;
|
2026-02-10 15:29:22 +00:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
var playerScript = FindObjectOfType<PlayerMovement>();
|
|
|
|
|
|
if (playerScript != null) target = playerScript.transform;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 3. 상태 봉인 (움직임 끄기)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (agent != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.enabled = false;
|
|
|
|
|
|
agent.isStopped = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(false);
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
|
|
|
|
|
// 4. 쇠공 초기 상태 설정 (물리 끄고 손에 붙이기)
|
|
|
|
|
|
if (ballRb != null) ballRb.isKinematic = true;
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void ExecuteAILogic()
|
|
|
|
|
|
{
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 전투 미시작 or 타겟 없음 -> 정지
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (!isBattleStarted || target == null) return;
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// ⭐ [최우선 순위] 무기가 없으면 공격 중단하고 주으러 감
|
|
|
|
|
|
if (isWeaponless)
|
|
|
|
|
|
{
|
|
|
|
|
|
RetrieveWeaponLogic();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- 일반 전투 로직 ---
|
|
|
|
|
|
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// 1. 공격 쿨타임 체크
|
|
|
|
|
|
_timer -= Time.deltaTime;
|
|
|
|
|
|
if (_timer <= 0 && !isAttacking && !isHit && !isDead)
|
|
|
|
|
|
{
|
|
|
|
|
|
_timer = patternInterval;
|
|
|
|
|
|
DecideAttack();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 2. 평소 이동 (플레이어 추격)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (!isAttacking && agent.enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.SetDestination(target.position);
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 애니메이션 속도 동기화
|
|
|
|
|
|
if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude);
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 거리 체크
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (agent.remainingDistance <= agent.stoppingDistance)
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.isStopped = true;
|
2026-02-11 07:21:58 +00:00
|
|
|
|
LookAtTarget(target.position);
|
2026-02-10 15:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.isStopped = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 🏃♂️ 무기 회수 로직 (Retrieve System)
|
|
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
private void RetrieveWeaponLogic()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (ironBall == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 공 위치로 이동
|
|
|
|
|
|
if (agent.enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.SetDestination(ironBall.transform.position);
|
|
|
|
|
|
agent.isStopped = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 이동 애니메이션
|
|
|
|
|
|
if (animator != null) animator.SetFloat("Speed", agent.velocity.magnitude);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 공과의 거리 체크 (가까워지면 줍기 시도)
|
|
|
|
|
|
float dist = Vector3.Distance(transform.position, ironBall.transform.position);
|
|
|
|
|
|
|
|
|
|
|
|
// 거리 3.0f 이내이고, 줍는 중이 아닐 때 실행
|
|
|
|
|
|
if (dist <= 3.0f && !isAttacking)
|
|
|
|
|
|
{
|
|
|
|
|
|
StartCoroutine(PickUpBallRoutine());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private IEnumerator PickUpBallRoutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart(); // 행동 시작 (다른 행동 불가)
|
|
|
|
|
|
|
|
|
|
|
|
// 멈춤
|
|
|
|
|
|
if (agent != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
agent.isStopped = true;
|
|
|
|
|
|
agent.velocity = Vector3.zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (animator != null) animator.SetFloat("Speed", 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 줍는 모션 재생
|
|
|
|
|
|
Debug.Log("boss: 내 소중한 공!!");
|
|
|
|
|
|
if (animator != null) animator.Play("Skill_Pickup"); // 줍는 애니메이션
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(0.8f); // 손이 바닥에 닿을 때까지 대기
|
|
|
|
|
|
|
|
|
|
|
|
// 2. ⭐ 공을 다시 손에 붙이기 (Parent)
|
|
|
|
|
|
ironBall.transform.SetParent(handHolder);
|
|
|
|
|
|
|
|
|
|
|
|
// 위치 초기화 (손바닥 안으로)
|
|
|
|
|
|
ironBall.transform.localPosition = Vector3.zero;
|
|
|
|
|
|
ironBall.transform.localRotation = Quaternion.identity;
|
|
|
|
|
|
|
|
|
|
|
|
// 쇠공 물리 끄기 (다시 무기화)
|
|
|
|
|
|
if (ballRb != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
ballRb.isKinematic = true;
|
|
|
|
|
|
ballRb.velocity = Vector3.zero;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f); // 일어나는 시간
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 상태 복구
|
|
|
|
|
|
isWeaponless = false; // 무기 장착 완료!
|
|
|
|
|
|
OnAttackEnd();
|
|
|
|
|
|
|
|
|
|
|
|
if (agent != null) agent.isStopped = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
// 🎬 전투 입장 & 유틸리티
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
public void StartBossBattle()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (isBattleStarted) return;
|
|
|
|
|
|
StartCoroutine(BattleStartRoutine());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private IEnumerator BattleStartRoutine()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (animator != null) animator.Play("Roar");
|
|
|
|
|
|
if (bossHealthBar != null) bossHealthBar.SetActive(true);
|
|
|
|
|
|
if (counterSystem != null) counterSystem.InitializeBattle();
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
yield return new WaitForSeconds(2.0f);
|
2026-02-10 15:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
isBattleStarted = true;
|
|
|
|
|
|
if (agent != null) agent.enabled = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
private void LookAtTarget(Vector3 targetPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 dir = targetPos - transform.position;
|
|
|
|
|
|
dir.y = 0;
|
|
|
|
|
|
if (dir != Vector3.zero)
|
|
|
|
|
|
{
|
|
|
|
|
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), Time.deltaTime * 5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 🧠 AI 공격 판단
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
private void DecideAttack()
|
|
|
|
|
|
{
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 이동 애니메이션 잠시 0으로
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (animator != null) animator.SetFloat("Speed", 0f);
|
|
|
|
|
|
|
|
|
|
|
|
string patternName = (counterSystem != null) ? counterSystem.SelectBossPattern() : "Normal";
|
|
|
|
|
|
|
|
|
|
|
|
switch (patternName)
|
|
|
|
|
|
{
|
2026-02-11 07:21:58 +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-11 07:21:58 +00:00
|
|
|
|
// ⚔️ 공격 패턴 (쇠공 버전)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
// ════════════════════════════════════════
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 1. 탱크 돌격 (Dash)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
private IEnumerator Pattern_DashAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart();
|
|
|
|
|
|
if (animator != null) animator.Play("Skill_Dash_Ready");
|
|
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// 돌진 (물리 이동)
|
|
|
|
|
|
if (agent != null) agent.enabled = false;
|
|
|
|
|
|
if (rb != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
rb.isKinematic = false;
|
|
|
|
|
|
rb.velocity = transform.forward * 20f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (animator != null) animator.Play("Skill_Dash_Go");
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f);
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 정지
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (rb != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
rb.velocity = Vector3.zero;
|
|
|
|
|
|
rb.isKinematic = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (agent != null && isBattleStarted) agent.enabled = true;
|
|
|
|
|
|
OnAttackEnd();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 2. 메테오 스매시 (Smash)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
private IEnumerator Pattern_SmashAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart();
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 엇박자 위협 (기 모으기)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (animator != null) animator.Play("Skill_Smash_Charge");
|
|
|
|
|
|
yield return new WaitForSeconds(1.2f);
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 쾅!
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (animator != null) animator.Play("Skill_Smash_Impact");
|
|
|
|
|
|
yield return new WaitForSeconds(1.0f);
|
|
|
|
|
|
OnAttackEnd();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 3. 쇠공 풍차 돌리기 (Shield)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
private IEnumerator Pattern_ShieldWall()
|
|
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart();
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 쇠공을 빙빙 돌림 (화살 튕겨내기)
|
2026-02-10 15:29:22 +00:00
|
|
|
|
if (animator != null) animator.Play("Skill_Shield");
|
|
|
|
|
|
yield return new WaitForSeconds(2.0f);
|
|
|
|
|
|
OnAttackEnd();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-11 07:21:58 +00:00
|
|
|
|
// 4. 공 던지기 (Throw - 기존 Shoot 대체)
|
|
|
|
|
|
private IEnumerator Pattern_ThrowBall()
|
2026-02-10 15:29:22 +00:00
|
|
|
|
{
|
|
|
|
|
|
OnAttackStart();
|
2026-02-11 07:21:58 +00:00
|
|
|
|
|
|
|
|
|
|
// 던지는 모션
|
|
|
|
|
|
if (animator != null) animator.Play("Attack_Throw");
|
|
|
|
|
|
yield return new WaitForSeconds(0.5f); // 손이 앞으로 뻗는 타이밍
|
|
|
|
|
|
|
|
|
|
|
|
// ⭐ 공 발사 로직
|
|
|
|
|
|
if (ironBall != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 손에서 떼어내기 (부모 해제)
|
|
|
|
|
|
ironBall.transform.SetParent(null);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 물리 켜고 날리기
|
|
|
|
|
|
if (ballRb != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
ballRb.isKinematic = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 플레이어 방향 계산
|
|
|
|
|
|
Vector3 dir = (target.position - transform.position).normalized;
|
|
|
|
|
|
// 위쪽으로 살짝 띄워서 포물선으로 던짐
|
|
|
|
|
|
ballRb.AddForce(dir * 1000f + Vector3.up * 300f);
|
|
|
|
|
|
|
|
|
|
|
|
// 굴러가지 않게 저항 좀 주기
|
|
|
|
|
|
ballRb.angularDrag = 5f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// "나 무기 없다!" 상태 설정 -> 이제부터 주으러 감
|
|
|
|
|
|
isWeaponless = true;
|
|
|
|
|
|
|
2026-02-10 15:29:22 +00:00
|
|
|
|
yield return new WaitForSeconds(1.0f);
|
|
|
|
|
|
OnAttackEnd();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|