feat: 드로그 보스 AI 및 런타임 상태 구조 재구성

- 드로그 전투 컨텍스트를 BossBehaviorRuntimeState 중심 구조로 정리하고 BossEnemy, 패턴 액션, 조건 노드가 마지막 실행 결과와 phase 상태를 직접 사용하도록 갱신
- BT_Drog와 재빌드 에디터 스크립트를 확장해 phase 전환, 집행 결과 분기, 거리/쿨타임 기반 패턴 선택을 드로그 전용 자산과 노드 파라미터로 재구성
- 드로그 패턴/스킬/이펙트/애니메이션 플레이스홀더 자산을 재생성하고 보스 프리팹이 새 런타임 상태 및 등록 클립 구성을 참조하도록 정리
This commit is contained in:
2026-04-06 13:56:47 +09:00
parent 60275c6cd9
commit 904bc88d36
172 changed files with 98477 additions and 3490 deletions

View File

@@ -28,6 +28,17 @@ namespace Colosseum.Skills
Revive,
}
/// <summary>
/// 마지막 스킬 실행 결과입니다.
/// </summary>
public enum SkillExecutionResult
{
None,
Running,
Completed,
Cancelled,
}
/// <summary>
/// 스킬 실행을 관리하는 컴포넌트.
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
@@ -61,6 +72,8 @@ namespace Colosseum.Skills
[SerializeField] private string lastCancelledSkillName = string.Empty;
[Tooltip("마지막 강제 취소 이유")]
[SerializeField] private SkillCancelReason lastCancelReason = SkillCancelReason.None;
[Tooltip("마지막 스킬 실행 결과")]
[SerializeField] private SkillExecutionResult lastExecutionResult = SkillExecutionResult.None;
// 현재 실행 중인 스킬
private SkillData currentSkill;
@@ -97,6 +110,7 @@ namespace Colosseum.Skills
public Animator Animator => animator;
public SkillCancelReason LastCancelReason => lastCancelReason;
public string LastCancelledSkillName => lastCancelledSkillName;
public SkillExecutionResult LastExecutionResult => lastExecutionResult;
public GameObject CurrentTargetOverride => currentTargetOverride;
public bool IsChannelingActive => isChannelingActive;
@@ -233,6 +247,8 @@ namespace Colosseum.Skills
{
if (currentSkill == null || animator == null) return;
UpdateCastTargetTracking();
// 채널링 중일 때
if (isChannelingActive)
{
@@ -263,7 +279,7 @@ namespace Colosseum.Skills
// 모든 클립과 반복이 끝나면 종료
if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}");
RestoreBaseController();
ClearCurrentSkillState();
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
}
}
@@ -275,6 +291,14 @@ namespace Colosseum.Skills
return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill));
}
/// <summary>
/// 타겟 오버라이드와 함께 스킬 시전
/// </summary>
public bool ExecuteSkill(SkillData skill, GameObject targetOverride)
{
return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill), targetOverride);
}
/// <summary>
/// 슬롯 엔트리 기준으로 스킬 시전
/// </summary>
@@ -340,6 +364,7 @@ namespace Colosseum.Skills
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
currentSkill = skill;
lastCancelReason = SkillCancelReason.None;
lastExecutionResult = SkillExecutionResult.Running;
BuildResolvedEffects(currentLoadoutEntry);
currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount();
currentIterationIndex = 0;
@@ -706,7 +731,7 @@ namespace Colosseum.Skills
Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}");
RestoreBaseController();
ClearCurrentSkillState();
CompleteCurrentSkillExecution(SkillExecutionResult.Cancelled);
return true;
}
@@ -945,7 +970,7 @@ namespace Colosseum.Skills
Debug.Log($"[Skill] 채널링 종료: {currentSkill?.SkillName}");
RestoreBaseController();
ClearCurrentSkillState();
CompleteCurrentSkillExecution(SkillExecutionResult.Completed);
}
/// <summary>
@@ -1023,6 +1048,65 @@ namespace Colosseum.Skills
currentIterationIndex = 0;
}
/// <summary>
/// 현재 시전 중인 스킬을 지정 결과로 종료합니다.
/// </summary>
private void CompleteCurrentSkillExecution(SkillExecutionResult result)
{
lastExecutionResult = result;
ClearCurrentSkillState();
}
/// <summary>
/// 적 스킬이 시전 중일 때 대상 추적 정책을 적용합니다.
/// </summary>
private void UpdateCastTargetTracking()
{
if (currentSkill == null || currentTargetOverride == null || !currentTargetOverride.activeInHierarchy)
return;
var enemyBase = GetComponent<Colosseum.Enemy.EnemyBase>();
if (enemyBase == null)
return;
if (IsSpawned && !IsServer)
return;
Vector3 direction = currentTargetOverride.transform.position - transform.position;
direction.y = 0f;
if (direction.sqrMagnitude < 0.0001f)
return;
if (currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.FaceTarget ||
currentSkill.CastTargetTrackingMode == SkillCastTargetTrackingMode.MoveTowardTarget)
{
Quaternion targetRotation = Quaternion.LookRotation(direction.normalized);
float rotationSpeed = Mathf.Max(0f, currentSkill.CastTargetRotationSpeed);
if (rotationSpeed <= 0f)
transform.rotation = targetRotation;
else
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, rotationSpeed * 360f * Time.deltaTime);
}
if (currentSkill.CastTargetTrackingMode != SkillCastTargetTrackingMode.MoveTowardTarget || currentSkill.UseRootMotion)
return;
UnityEngine.AI.NavMeshAgent navMeshAgent = GetComponent<UnityEngine.AI.NavMeshAgent>();
if (navMeshAgent == null || !navMeshAgent.enabled)
return;
float stopDistance = Mathf.Max(0f, currentSkill.CastTargetStopDistance);
if (direction.magnitude <= stopDistance)
{
navMeshAgent.isStopped = true;
navMeshAgent.ResetPath();
return;
}
navMeshAgent.isStopped = false;
navMeshAgent.SetDestination(currentTargetOverride.transform.position);
}
/// <summary>
/// 현재 트리거 인덱스에 연결된 젬 이상상태를 적중 대상에게 적용합니다.
/// </summary>