#if UNITY_EDITOR || DEVELOPMENT_BUILD using System.Collections; using Unity.Behavior; using UnityEngine; using Colosseum.Abnormalities; using Colosseum.AI; using Colosseum.Combat; using Colosseum.Player; using Colosseum.Skills; namespace Colosseum.Enemy { /// /// 디버그 패턴 강제 발동 중 BT 개입 방식을 정의합니다. /// public enum DebugPatternBehaviorTreeMode { DisableDuringPattern = 0, KeepRunning = 1, } /// /// 디버그 패널에서 보스 패턴을 임의 발동할 때 사용하는 런타임 실행기입니다. /// [DisallowMultipleComponent] [RequireComponent(typeof(BossEnemy))] [RequireComponent(typeof(BossBehaviorRuntimeState))] [RequireComponent(typeof(SkillController))] public class BossPatternDebugRunner : MonoBehaviour { [Header("Debug")] [Tooltip("패턴 디버그 실행기 로그 출력 여부")] [SerializeField] private bool debugMode; private BossEnemy bossEnemy; private EnemyBase enemyBase; private SkillController skillController; private BossBehaviorRuntimeState runtimeState; private AbnormalityManager abnormalityManager; private UnityEngine.AI.NavMeshAgent navMeshAgent; private BehaviorGraphAgent behaviorGraphAgent; private Coroutine activePatternRoutine; private BossPatternData currentPattern; private GameObject currentTarget; private ChargeStepData activeChargeData; private float chargeEndTime; private float chargeAccumulatedDamage; private float chargeRequiredDamage; private bool isChargeWaiting; private bool chargeTelegraphApplied; private bool restoreBehaviorGraphWhenDebugPauseEnds; private bool debugBehaviorGraphPaused; /// /// 현재 디버그 패턴 실행 중 여부입니다. /// public bool IsExecutingPattern => activePatternRoutine != null; /// /// 현재 디버그 실행 중인 패턴입니다. /// public BossPatternData CurrentPattern => currentPattern; /// /// 디버그 토글로 BT가 중지된 상태인지 반환합니다. /// public bool IsBehaviorTreePaused => debugBehaviorGraphPaused; private bool IsServer => Unity.Netcode.NetworkManager.Singleton != null && Unity.Netcode.NetworkManager.Singleton.IsServer; private void Awake() { ResolveReferences(); } private void Update() { if (!IsExecutingPattern) return; StopMovement(); } private void OnDisable() { CancelPattern(); } /// /// 지정한 패턴을 디버그 용도로 즉시 실행합니다. /// public bool TryExecutePattern(BossPatternData pattern, DebugPatternBehaviorTreeMode behaviorTreeMode = DebugPatternBehaviorTreeMode.DisableDuringPattern) { if (!IsServer) { Debug.LogWarning("[BossPatternDebugRunner] 서버가 아니어서 패턴을 강제 발동할 수 없습니다.", this); return false; } ResolveReferences(); if (pattern == null) { Debug.LogWarning("[BossPatternDebugRunner] 발동할 패턴이 비어 있습니다.", this); return false; } if (bossEnemy == null || enemyBase == null || skillController == null || runtimeState == null) { Debug.LogWarning("[BossPatternDebugRunner] 필수 컴포넌트를 찾지 못해 패턴을 실행할 수 없습니다.", this); return false; } if (bossEnemy.IsDead) { Debug.LogWarning($"[BossPatternDebugRunner] 사망 상태에서는 패턴을 실행할 수 없습니다: {pattern.PatternName}", this); return false; } CancelPattern(); if (skillController.IsExecutingSkill) skillController.CancelSkill(SkillCancelReason.Interrupt); currentTarget = ResolvePatternTarget(pattern); currentPattern = pattern; runtimeState.WasChargeBroken = false; runtimeState.LastChargeStaggerDuration = 0f; ApplyPatternFlowState(pattern); runtimeState.SetCurrentTarget(currentTarget); runtimeState.BeginPatternExecution(pattern); StopMovement(); activePatternRoutine = StartCoroutine(RunPatternRoutine(pattern)); LogDebug($"디버그 패턴 발동: {pattern.PatternName} / Target={currentTarget?.name ?? "없음"} / BTMode={behaviorTreeMode}"); return true; } /// /// 현재 디버그 패턴 실행을 취소합니다. /// public void CancelPattern() { if (activePatternRoutine != null) { StopCoroutine(activePatternRoutine); activePatternRoutine = null; } if (currentPattern == null) return; if (skillController != null && skillController.IsExecutingSkill) skillController.CancelSkill(SkillCancelReason.Interrupt); FinalizePattern(BossPatternExecutionResult.Cancelled, applyCooldown: false); } /// /// 디버그 토글에 따라 BT를 지속적으로 정지하거나 재개합니다. /// public void SetBehaviorTreePaused(bool paused) { ResolveReferences(); SetBehaviorGraphSuspended(paused); } private IEnumerator RunPatternRoutine(BossPatternData pattern) { BossPatternExecutionResult result = BossPatternExecutionResult.Cancelled; bool applyCooldown = false; try { for (int i = 0; i < pattern.Steps.Count; i++) { PatternStep step = pattern.Steps[i]; if (step.Type == PatternStepType.Wait) { yield return WaitForSeconds(step.Duration); continue; } if (step.Type == PatternStepType.ChargeWait) { bool chargeBroken = false; yield return RunChargeWait(step, value => chargeBroken = value); if (chargeBroken) { runtimeState.WasChargeBroken = true; applyCooldown = true; result = BossPatternExecutionResult.Failed; yield break; } continue; } if (step.Skill == null) { Debug.LogWarning($"[BossPatternDebugRunner] 스킬이 비어 있는 패턴 스텝입니다: {pattern.PatternName} / Step={i}", this); result = BossPatternExecutionResult.Failed; yield break; } GameObject stepTarget = currentTarget; if (step.Skill.JumpToTarget) { stepTarget = ResolveJumpTarget(); if (stepTarget == null) { if (pattern.SkipJumpStepOnNoTarget) { applyCooldown = true; result = BossPatternExecutionResult.Succeeded; LogDebug($"점프 대상 없음, 디버그 패턴 조기 종료: {pattern.PatternName}"); yield break; } Debug.LogWarning($"[BossPatternDebugRunner] 점프 타겟을 찾지 못했습니다: {pattern.PatternName}", this); result = BossPatternExecutionResult.Failed; yield break; } enemyBase.SetJumpTarget(stepTarget.transform.position); } bool executed = stepTarget != null ? skillController.ExecuteSkill(step.Skill, stepTarget) : skillController.ExecuteSkill(step.Skill); if (!executed) { Debug.LogWarning($"[BossPatternDebugRunner] 스킬 실행 실패: {step.Skill.SkillName}", this); result = BossPatternExecutionResult.Failed; yield break; } LogDebug($"디버그 패턴 실행: {pattern.PatternName} / Step={i} / Skill={step.Skill.SkillName}"); while (skillController.IsPlayingAnimation) yield return null; if (skillController.LastExecutionResult != SkillExecutionResult.Completed) { result = BossPatternExecutionResult.Cancelled; yield break; } } applyCooldown = true; result = BossPatternExecutionResult.Succeeded; } finally { activePatternRoutine = null; FinalizePattern(result, applyCooldown); } } private IEnumerator RunChargeWait(PatternStep step, System.Action onFinished) { bool broken = false; StartChargeWait(step); while (Time.time < chargeEndTime) { if (chargeAccumulatedDamage >= chargeRequiredDamage) { broken = true; break; } yield return null; } EndChargeWait(broken); if (broken && skillController != null && skillController.IsExecutingSkill) skillController.CancelSkill(SkillCancelReason.Interrupt); onFinished?.Invoke(broken); } private IEnumerator WaitForSeconds(float duration) { float endTime = Time.time + Mathf.Max(0f, duration); while (Time.time < endTime) yield return null; } private void StartChargeWait(PatternStep step) { isChargeWaiting = true; activeChargeData = step.ChargeData; chargeAccumulatedDamage = 0f; chargeTelegraphApplied = false; chargeRequiredDamage = bossEnemy.MaxHealth * (activeChargeData != null ? activeChargeData.RequiredDamageRatio : 0.1f); chargeEndTime = Time.time + Mathf.Max(0f, step.Duration); enemyBase.OnDamageTaken += OnChargeDamageTaken; if (activeChargeData != null && activeChargeData.TelegraphAbnormality != null && abnormalityManager != null) { abnormalityManager.ApplyAbnormality(activeChargeData.TelegraphAbnormality, gameObject); chargeTelegraphApplied = true; } LogDebug($"디버그 충전 대기 시작: 필요 피해={chargeRequiredDamage:F1} / 대기={step.Duration:F1}s"); } private void EndChargeWait(bool broken) { isChargeWaiting = false; enemyBase.OnDamageTaken -= OnChargeDamageTaken; if (chargeTelegraphApplied && abnormalityManager != null && activeChargeData != null && activeChargeData.TelegraphAbnormality != null) { abnormalityManager.RemoveAbnormality(activeChargeData.TelegraphAbnormality); } if (broken && activeChargeData != null) runtimeState.LastChargeStaggerDuration = activeChargeData.StaggerDuration; activeChargeData = null; chargeTelegraphApplied = false; } private void FinalizePattern(BossPatternExecutionResult result, bool applyCooldown) { if (isChargeWaiting) EndChargeWait(broken: false); if (currentPattern != null && runtimeState != null) { if (applyCooldown) runtimeState.SetPatternCooldown(currentPattern); runtimeState.CompletePatternExecution(currentPattern, result); } currentPattern = null; currentTarget = null; chargeAccumulatedDamage = 0f; chargeRequiredDamage = 0f; activeChargeData = null; chargeTelegraphApplied = false; } private void ApplyPatternFlowState(BossPatternData pattern) { if (runtimeState == null || pattern == null) return; if (pattern.Category == PatternCategory.Punish || pattern.IsBigPattern) { runtimeState.ResetBasicLoopCount(); return; } if (pattern.IsMelee) runtimeState.IncrementBasicLoopCount(); } private GameObject ResolvePatternTarget(BossPatternData pattern) { if (pattern == null) return null; return pattern.TargetMode switch { TargetResolveMode.None => null, TargetResolveMode.Mobility => ResolveMobilityTarget(), _ => ResolvePrimaryTarget(), }; } private GameObject ResolvePrimaryTarget() { GameObject currentRuntimeTarget = runtimeState != null ? runtimeState.CurrentTarget : null; if (IsValidHostileTarget(currentRuntimeTarget)) return currentRuntimeTarget; float aggroRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity; GameObject highestThreatTarget = enemyBase != null ? enemyBase.GetHighestThreatTarget(currentRuntimeTarget, null, aggroRange) : null; if (IsValidHostileTarget(highestThreatTarget)) return highestThreatTarget; return FindNearestLivingPlayer(); } private GameObject ResolveMobilityTarget() { PlayerNetworkController[] players = Object.FindObjectsByType(FindObjectsSortMode.None); if (players == null || players.Length == 0) return ResolvePrimaryTarget(); GameObject primaryTarget = ResolvePrimaryTarget(); GameObject farthestTarget = null; float farthestDistance = float.MinValue; float aggroRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity; for (int i = 0; i < players.Length; i++) { PlayerNetworkController player = players[i]; if (player == null || player.IsDead || !player.gameObject.activeInHierarchy) continue; GameObject candidate = player.gameObject; if (candidate == primaryTarget || !IsValidHostileTarget(candidate)) continue; float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(transform.position, candidate); if (distance > aggroRange || distance <= farthestDistance) continue; farthestDistance = distance; farthestTarget = candidate; } return farthestTarget != null ? farthestTarget : primaryTarget; } private GameObject ResolveJumpTarget() { GameObject resolvedTarget = IsValidJumpTarget(currentTarget) ? currentTarget : ResolvePatternTarget(currentPattern); currentTarget = resolvedTarget; runtimeState?.SetCurrentTarget(resolvedTarget); return resolvedTarget; } private GameObject FindNearestLivingPlayer() { PlayerNetworkController[] players = Object.FindObjectsByType(FindObjectsSortMode.None); GameObject nearestTarget = null; float nearestDistance = float.MaxValue; float aggroRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity; for (int i = 0; i < players.Length; i++) { PlayerNetworkController player = players[i]; if (player == null || player.IsDead || !player.gameObject.activeInHierarchy) continue; GameObject candidate = player.gameObject; if (!IsValidHostileTarget(candidate)) continue; float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(transform.position, candidate); if (distance > aggroRange || distance >= nearestDistance) continue; nearestDistance = distance; nearestTarget = candidate; } return nearestTarget; } private bool IsValidHostileTarget(GameObject candidate) { if (candidate == null || !candidate.activeInHierarchy) return false; if (Team.IsSameTeam(gameObject, candidate)) return false; IDamageable damageable = candidate.GetComponent(); return damageable == null || !damageable.IsDead; } private bool IsValidJumpTarget(GameObject candidate) { if (!IsValidHostileTarget(candidate)) return false; float aggroRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f; float distance = TargetSurfaceUtility.GetHorizontalSurfaceDistance(transform.position, candidate); return distance <= aggroRange; } private void OnChargeDamageTaken(float damage) { if (damage <= 0f) return; chargeAccumulatedDamage += damage; } private void StopMovement() { if (navMeshAgent == null || !navMeshAgent.enabled) return; navMeshAgent.isStopped = true; navMeshAgent.ResetPath(); } private void SetBehaviorGraphSuspended(bool suspended) { if (behaviorGraphAgent == null) return; if (suspended) { if (debugBehaviorGraphPaused) return; debugBehaviorGraphPaused = true; restoreBehaviorGraphWhenDebugPauseEnds = behaviorGraphAgent.enabled; behaviorGraphAgent.End(); if (restoreBehaviorGraphWhenDebugPauseEnds) behaviorGraphAgent.enabled = false; return; } if (!debugBehaviorGraphPaused) return; debugBehaviorGraphPaused = false; if (!restoreBehaviorGraphWhenDebugPauseEnds) return; restoreBehaviorGraphWhenDebugPauseEnds = false; if (bossEnemy != null && !bossEnemy.IsDead && behaviorGraphAgent.Graph != null) { behaviorGraphAgent.enabled = true; behaviorGraphAgent.Restart(); } } private void ResolveReferences() { if (bossEnemy == null) bossEnemy = GetComponent(); if (enemyBase == null) enemyBase = GetComponent(); if (skillController == null) skillController = GetComponent(); if (runtimeState == null) runtimeState = GetComponent(); if (abnormalityManager == null) abnormalityManager = GetComponent(); if (navMeshAgent == null) navMeshAgent = GetComponent(); if (behaviorGraphAgent == null) behaviorGraphAgent = GetComponent(); } private void LogDebug(string message) { if (debugMode) Debug.Log($"[BossPatternDebugRunner] {message}", this); } } } #endif