using System.Collections; using System.Collections.Generic; using UnityEngine; using Unity.Netcode; using Colosseum.AI; using Colosseum.Player; using Colosseum.Skills; namespace Colosseum.Enemy { /// /// 드로그 전용 패턴 선택 컨트롤러입니다. /// 기본 루프, 도약, 다운 추가타 같은 고우선 패턴을 직접 선택합니다. /// [DisallowMultipleComponent] [RequireComponent(typeof(BossEnemy))] [RequireComponent(typeof(SkillController))] public class DrogPatternController : NetworkBehaviour { [Header("References")] [SerializeField] private BossEnemy bossEnemy; [SerializeField] private EnemyBase enemyBase; [SerializeField] private SkillController skillController; [SerializeField] private UnityEngine.AI.NavMeshAgent navMeshAgent; [Header("Pattern Data")] [Tooltip("기본 근접 압박 패턴")] [SerializeField] private BossPatternData mainPattern; [Tooltip("먼 대상 징벌용 도약 패턴")] [SerializeField] private BossPatternData leapPattern; [Tooltip("다운 대상이 있을 때 우선 발동하는 광역 추가타 패턴")] [SerializeField] private BossPatternData downPunishPattern; [Header("Phase Thresholds")] [Tooltip("2페이즈 진입 체력 비율")] [Range(0f, 1f)] [SerializeField] private float phase2HealthThreshold = 0.75f; [Tooltip("3페이즈 진입 체력 비율")] [Range(0f, 1f)] [SerializeField] private float phase3HealthThreshold = 0.4f; [Header("Targeting")] [Tooltip("타겟 재탐색 주기")] [Min(0.05f)] [SerializeField] private float targetRefreshInterval = 0.2f; [Tooltip("도약 패턴을 고려하기 시작하는 거리")] [Min(0f)] [SerializeField] private float leapDistanceThreshold = 8f; [Tooltip("다운 추가타를 고려할 최대 반경")] [Min(0f)] [SerializeField] private float downPunishSearchRadius = 6f; [Header("Behavior")] [Tooltip("드로그 전용 컨트롤러 사용 시 기존 BehaviorGraph를 비활성화할지 여부")] [SerializeField] private bool disableBehaviorGraph = true; [Tooltip("디버그 로그 출력 여부")] [SerializeField] private bool debugMode = false; private readonly Dictionary patternCooldownTracker = new Dictionary(); private Coroutine activePatternCoroutine; private GameObject currentTarget; private float nextTargetRefreshTime; /// /// 드로그 컨트롤러 사용 시 BehaviorGraph를 비활성화할지 여부 /// public bool DisableBehaviorGraph => disableBehaviorGraph; /// /// 현재 전용 패턴 실행 중인지 여부 /// public bool IsExecutingPattern => activePatternCoroutine != null; /// /// 현재 드로그 패턴 페이즈 /// public int CurrentPatternPhase { get { float healthRatio = bossEnemy != null && bossEnemy.MaxHealth > 0f ? bossEnemy.CurrentHealth / bossEnemy.MaxHealth : 1f; if (healthRatio <= phase3HealthThreshold) return 3; if (healthRatio <= phase2HealthThreshold) return 2; return 1; } } private void Awake() { ResolveReferences(); } public override void OnNetworkSpawn() { ResolveReferences(); if (!IsServer) { enabled = false; } } private void Update() { if (!IsServer) return; ResolveReferences(); if (bossEnemy == null || enemyBase == null || skillController == null) return; if (bossEnemy.IsDead || bossEnemy.IsTransitioning) return; RefreshTargetIfNeeded(); UpdateMovement(); if (activePatternCoroutine != null || skillController.IsPlayingAnimation) return; if (TryStartDownPunishPattern()) return; TryStartMainPattern(); } private bool TryStartDownPunishPattern() { if (!IsPatternReady(downPunishPattern)) return false; GameObject downedTarget = FindNearestDownedTarget(); if (downedTarget == null) return false; StartPattern(downPunishPattern, downedTarget); return true; } private bool TryStartMainPattern() { if (currentTarget == null) return false; float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position); if (distanceToTarget >= leapDistanceThreshold && IsPatternReady(leapPattern)) { StartPattern(leapPattern, currentTarget); return true; } float attackRange = enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f; if (distanceToTarget <= attackRange + 0.25f && IsPatternReady(mainPattern)) { StartPattern(mainPattern, currentTarget); return true; } return false; } private void StartPattern(BossPatternData pattern, GameObject target) { if (pattern == null || activePatternCoroutine != null) return; if (debugMode) { string targetName = target != null ? target.name : "None"; Debug.Log($"[DrogPattern] 패턴 시작: {pattern.PatternName} / Target={targetName} / Phase={CurrentPatternPhase}"); } activePatternCoroutine = StartCoroutine(RunPatternCoroutine(pattern, target)); } private IEnumerator RunPatternCoroutine(BossPatternData pattern, GameObject target) { StopMovement(); bool completed = true; BossPatternData chainedPattern = null; GameObject chainedTarget = null; for (int i = 0; i < pattern.Steps.Count; i++) { PatternStep step = pattern.Steps[i]; if (step.Type == PatternStepType.Wait) { yield return new WaitForSeconds(step.Duration); continue; } if (step.Skill == null) { completed = false; Debug.LogWarning($"[DrogPattern] 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}"); break; } if (step.Skill.JumpToTarget && target != null) { enemyBase?.SetJumpTarget(target.transform.position); } if (!skillController.ExecuteSkill(step.Skill)) { completed = false; if (debugMode) { Debug.LogWarning($"[DrogPattern] 스킬 실행 실패: {step.Skill.SkillName}"); } break; } yield return new WaitUntil(() => skillController == null || !skillController.IsPlayingAnimation || bossEnemy == null || bossEnemy.IsDead); if (bossEnemy == null || bossEnemy.IsDead) { completed = false; break; } if (pattern != downPunishPattern && TryPrepareDownPunishChain(out chainedTarget)) { chainedPattern = downPunishPattern; break; } } if (completed) { patternCooldownTracker[pattern] = Time.time + pattern.Cooldown; } activePatternCoroutine = null; if (chainedPattern != null && chainedTarget != null && bossEnemy != null && !bossEnemy.IsDead) { StartPattern(chainedPattern, chainedTarget); } } private bool IsPatternReady(BossPatternData pattern) { if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0) return false; if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime)) return true; return Time.time >= readyTime; } private void RefreshTargetIfNeeded() { if (Time.time < nextTargetRefreshTime) return; nextTargetRefreshTime = Time.time + targetRefreshInterval; GameObject highestThreatTarget = enemyBase.GetHighestThreatTarget(currentTarget, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity); currentTarget = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingPlayer(); } private void UpdateMovement() { if (navMeshAgent == null || !navMeshAgent.enabled) return; if (activePatternCoroutine != null || (skillController != null && skillController.IsPlayingAnimation)) { StopMovement(); return; } if (currentTarget == null) { StopMovement(); return; } float attackRange = enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f; float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position); if (distanceToTarget <= attackRange) { StopMovement(); return; } navMeshAgent.isStopped = false; navMeshAgent.SetDestination(currentTarget.transform.position); } private void StopMovement() { if (navMeshAgent == null || !navMeshAgent.enabled) return; navMeshAgent.isStopped = true; navMeshAgent.ResetPath(); } private GameObject FindNearestDownedTarget() { HitReactionController[] hitReactionControllers = FindObjectsByType(FindObjectsSortMode.None); GameObject bestTarget = null; float bestDistance = float.MaxValue; for (int i = 0; i < hitReactionControllers.Length; i++) { HitReactionController controller = hitReactionControllers[i]; if (controller == null || !controller.IsDowned) continue; GameObject candidate = controller.gameObject; if (candidate == null || !candidate.activeInHierarchy || Team.IsSameTeam(gameObject, candidate)) continue; float distance = Vector3.Distance(transform.position, candidate.transform.position); if (distance > downPunishSearchRadius || distance >= bestDistance) continue; bestDistance = distance; bestTarget = candidate; } return bestTarget; } private bool TryPrepareDownPunishChain(out GameObject downedTarget) { downedTarget = null; if (!IsPatternReady(downPunishPattern)) return false; downedTarget = FindNearestDownedTarget(); if (downedTarget == null) return false; if (debugMode) { Debug.Log($"[DrogPattern] 다운 대상 감지, 다운 추가타 연계 준비: {downedTarget.name}"); } return true; } private GameObject FindNearestLivingPlayer() { PlayerNetworkController[] players = FindObjectsByType(FindObjectsSortMode.None); GameObject bestTarget = null; float bestDistance = float.MaxValue; float aggroRange = 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 (Team.IsSameTeam(gameObject, candidate)) continue; float distance = Vector3.Distance(transform.position, candidate.transform.position); if (distance > aggroRange || distance >= bestDistance) continue; bestDistance = distance; bestTarget = candidate; } return bestTarget; } private void ResolveReferences() { if (bossEnemy == null) bossEnemy = GetComponent(); if (enemyBase == null) enemyBase = GetComponent(); if (skillController == null) skillController = GetComponent(); if (navMeshAgent == null) navMeshAgent = GetComponent(); } } }