using System.Collections; using System.Collections.Generic; using Colosseum.AI; using Colosseum.Combat; using Colosseum.Player; using Colosseum.Skills; using Unity.Behavior; using Unity.Netcode; using UnityEngine; using UnityEngine.Serialization; namespace Colosseum.Enemy { /// /// 보스 공통 전투 BT가 참조하는 전투 컨텍스트입니다. /// 패턴 슬롯, 거리 기준, 페이즈별 주기, 공통 타겟 판정 정보를 제공합니다. /// [DisallowMultipleComponent] [RequireComponent(typeof(BossEnemy))] [RequireComponent(typeof(SkillController))] public abstract class BossCombatBehaviorContext : NetworkBehaviour { [Header("References")] [SerializeField] protected BossEnemy bossEnemy; [SerializeField] protected EnemyBase enemyBase; [SerializeField] protected SkillController skillController; [SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent; [SerializeField] protected BehaviorGraphAgent behaviorGraphAgent; [Header("Pattern Data")] [Tooltip("기본 근접 압박 패턴")] [FormerlySerializedAs("mainPattern")] [SerializeField] protected BossPatternData primaryPattern; [Tooltip("보조 근접 압박 패턴")] [FormerlySerializedAs("slamPattern")] [SerializeField] protected BossPatternData secondaryPattern; [Tooltip("기동 또는 거리 징벌 패턴")] [FormerlySerializedAs("leapPattern")] [SerializeField] protected BossPatternData mobilityPattern; [Tooltip("특정 상황에서 우선 발동하는 징벌 패턴")] [FormerlySerializedAs("downPunishPattern")] [SerializeField] protected BossPatternData punishPattern; [Header("Phase Thresholds")] [Tooltip("2페이즈 진입 체력 비율")] [Range(0f, 1f)] [SerializeField] protected float phase2HealthThreshold = 0.75f; [Tooltip("3페이즈 진입 체력 비율")] [Range(0f, 1f)] [SerializeField] protected float phase3HealthThreshold = 0.4f; [Header("Targeting")] [Tooltip("타겟 재탐색 주기")] [FormerlySerializedAs("targetRefreshInterval")] [Min(0.05f)] [SerializeField] protected float primaryTargetRefreshInterval = 0.2f; [Tooltip("기동 패턴을 고려하기 시작하는 거리")] [FormerlySerializedAs("leapDistanceThreshold")] [Min(0f)] [SerializeField] protected float mobilityTriggerDistance = 8f; [Tooltip("징벌 패턴을 고려할 최대 반경")] [FormerlySerializedAs("downPunishSearchRadius")] [Min(0f)] [SerializeField] protected float punishSearchRadius = 6f; [Header("Pattern Cadence")] [Tooltip("1페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")] [FormerlySerializedAs("phase1SlamInterval")] [Min(1)] [SerializeField] protected int phase1SecondaryInterval = 3; [Tooltip("2페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")] [FormerlySerializedAs("phase2SlamInterval")] [Min(1)] [SerializeField] protected int phase2SecondaryInterval = 2; [Tooltip("3페이즈에서 몇 번의 근접 패턴마다 보조 패턴을 섞을지")] [FormerlySerializedAs("phase3SlamInterval")] [Min(1)] [SerializeField] protected int phase3SecondaryInterval = 2; [Header("Behavior")] [Tooltip("전용 컨텍스트 사용 시 기존 BehaviorGraph를 비활성화할지 여부")] [SerializeField] protected bool disableBehaviorGraph = true; [Tooltip("디버그 로그 출력 여부")] [SerializeField] protected bool debugMode = false; protected readonly Dictionary patternCooldownTracker = new Dictionary(); protected Coroutine activePatternCoroutine; protected GameObject currentTarget; protected float nextTargetRefreshTime; protected int meleePatternCounter; /// /// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부 /// public bool DisableBehaviorGraph => disableBehaviorGraph; /// /// 기동 패턴을 고려하는 최소 거리 /// public float MobilityTriggerDistance => mobilityTriggerDistance; /// /// 징벌 패턴을 고려하는 최대 반경 /// public float PunishSearchRadius => punishSearchRadius; /// /// 디버그 로그 출력 여부 /// public bool DebugModeEnabled => debugMode; /// /// 현재 보스 패턴 페이즈 /// 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; } } protected virtual void Awake() { ResolveReferences(); } public override void OnNetworkSpawn() { ResolveReferences(); if (!IsServer) enabled = false; } protected virtual void Update() { if (!IsServer) return; ResolveReferences(); if (bossEnemy == null || enemyBase == null || skillController == null) return; if (bossEnemy.IsDead || bossEnemy.IsTransitioning) return; if (!disableBehaviorGraph) return; RefreshTargetIfNeeded(); UpdateMovement(); if (skillController.IsPlayingAnimation) return; if (TryStartMobilityPattern()) return; TryStartPrimaryLoopPattern(); } /// /// 현재 역할의 패턴 데이터를 반환합니다. /// public BossPatternData GetPattern(BossCombatPatternRole role) { return role switch { BossCombatPatternRole.Primary => primaryPattern, BossCombatPatternRole.Secondary => secondaryPattern, BossCombatPatternRole.Mobility => mobilityPattern, BossCombatPatternRole.Punish => punishPattern, _ => null, }; } /// /// 다음 근접 패턴 차례가 보조 패턴인지 여부 /// public bool IsNextSecondaryPattern() { int secondaryInterval = GetSecondaryIntervalForPhase(CurrentPatternPhase); if (secondaryInterval <= 1) return true; return (meleePatternCounter + 1) % secondaryInterval == 0; } /// /// 현재 페이즈 기준의 보조 근접 패턴 주기를 반환합니다. /// public int GetSecondaryIntervalForPhase(int phase) { return phase switch { 1 => Mathf.Max(1, phase1SecondaryInterval), 2 => Mathf.Max(1, phase2SecondaryInterval), _ => Mathf.Max(1, phase3SecondaryInterval), }; } /// /// 근접 패턴 사용 카운터를 갱신합니다. /// public void RegisterPatternUse(BossCombatPatternRole role) { if (!role.IsMeleeRole()) return; meleePatternCounter++; } /// /// 살아 있는 적대 대상인지 확인합니다. /// public 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; } /// /// 기동 패턴 대상으로 유효한지 확인합니다. /// public bool IsValidMobilityTarget(GameObject candidate) { if (!IsValidHostileTarget(candidate)) return false; float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f; float distance = Vector3.Distance(transform.position, candidate.transform.position); return distance >= mobilityTriggerDistance && distance <= maxDistance; } /// /// 기동 패턴 대상으로 사용할 수 있는 가장 먼 유효 타겟을 찾습니다. /// public GameObject FindMobilityTarget() { GameObject[] candidates = GameObject.FindGameObjectsWithTag("Player"); GameObject farthestTarget = null; float bestDistance = mobilityTriggerDistance; float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f; for (int i = 0; i < candidates.Length; i++) { GameObject candidate = candidates[i]; if (!IsValidMobilityTarget(candidate)) continue; float distance = Vector3.Distance(transform.position, candidate.transform.position); if (distance > maxDistance || distance <= bestDistance) continue; bestDistance = distance; farthestTarget = candidate; } return farthestTarget; } /// /// 가장 가까운 생존 플레이어를 찾습니다. /// public GameObject FindNearestLivingTarget() { PlayerNetworkController[] players = 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 = Vector3.Distance(transform.position, candidate.transform.position); if (distance > aggroRange || distance >= nearestDistance) continue; nearestDistance = distance; nearestTarget = candidate; } return nearestTarget; } /// /// 로그를 출력합니다. /// public void LogDebug(string source, string message) { if (debugMode) Debug.Log($"[{source}] {message}"); } protected virtual bool TryStartPrimaryLoopPattern() { if (currentTarget == null) return false; float distanceToTarget = Vector3.Distance(transform.position, currentTarget.transform.position); float attackRange = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AttackRange : 2f; if (distanceToTarget > attackRange + 0.25f) return false; BossPatternData selectedPattern = SelectPrimaryLoopPattern(); if (selectedPattern == null) return false; StartPattern(selectedPattern, currentTarget); return true; } protected virtual bool TryStartMobilityPattern() { BossPatternData pattern = GetPattern(BossCombatPatternRole.Mobility); if (!IsPatternReady(pattern)) return false; GameObject target = FindMobilityTarget(); if (target == null) return false; currentTarget = target; StartPattern(pattern, target); return true; } protected virtual BossPatternData SelectPrimaryLoopPattern() { BossPatternData primary = GetPattern(BossCombatPatternRole.Primary); BossPatternData secondary = GetPattern(BossCombatPatternRole.Secondary); bool canUsePrimary = IsPatternReady(primary); bool canUseSecondary = IsPatternReady(secondary); if (canUseSecondary && IsNextSecondaryPattern()) { meleePatternCounter++; return secondary; } if (canUsePrimary) { meleePatternCounter++; return primary; } if (canUseSecondary) { meleePatternCounter++; return secondary; } return null; } protected virtual void StartPattern(BossPatternData pattern, GameObject target) { if (pattern == null || activePatternCoroutine != null) return; LogDebug(GetType().Name, $"패턴 시작: {pattern.PatternName} / Target={(target != null ? target.name : "None")} / Phase={CurrentPatternPhase}"); activePatternCoroutine = StartCoroutine(RunPatternCoroutine(pattern, target)); } protected virtual IEnumerator RunPatternCoroutine(BossPatternData pattern, GameObject target) { StopMovement(); bool completed = true; 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($"[{GetType().Name}] 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}"); break; } if (step.Skill.JumpToTarget && target != null) { enemyBase?.SetJumpTarget(target.transform.position); } if (!skillController.ExecuteSkill(step.Skill)) { completed = false; LogDebug(GetType().Name, $"스킬 실행 실패: {step.Skill.SkillName}"); break; } yield return new WaitUntil(() => skillController == null || !skillController.IsPlayingAnimation || bossEnemy == null || bossEnemy.IsDead); if (bossEnemy == null || bossEnemy.IsDead) break; } if (completed) { patternCooldownTracker[pattern] = Time.time + pattern.Cooldown; } activePatternCoroutine = null; currentTarget = target; } protected 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; } protected virtual void RefreshTargetIfNeeded() { if (Time.time < nextTargetRefreshTime) return; nextTargetRefreshTime = Time.time + primaryTargetRefreshInterval; GameObject highestThreatTarget = enemyBase.GetHighestThreatTarget(currentTarget, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity); currentTarget = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingTarget(); } protected virtual void UpdateMovement() { if (navMeshAgent == null || !navMeshAgent.enabled) return; if (skillController != null && skillController.IsPlayingAnimation) { StopMovement(); return; } if (currentTarget == null) { StopMovement(); return; } float attackRange = enemyBase != null && 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.stoppingDistance = attackRange; navMeshAgent.SetDestination(currentTarget.transform.position); } protected void StopMovement() { if (navMeshAgent == null || !navMeshAgent.enabled) return; navMeshAgent.isStopped = true; navMeshAgent.ResetPath(); } protected virtual void ResolveReferences() { if (bossEnemy == null) bossEnemy = GetComponent(); if (enemyBase == null) enemyBase = GetComponent(); if (skillController == null) skillController = GetComponent(); if (navMeshAgent == null) navMeshAgent = GetComponent(); if (behaviorGraphAgent == null) behaviorGraphAgent = GetComponent(); } } }