using System.Collections; using System.Collections.Generic; using Colosseum.AI; using Colosseum.Abnormalities; 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 AbnormalityManager abnormalityManager; [SerializeField] protected UnityEngine.AI.NavMeshAgent navMeshAgent; [SerializeField] protected BehaviorGraphAgent behaviorGraphAgent; [Header("Pattern Data")] [Tooltip("기본 근접 압박 패턴")] [FormerlySerializedAs("mainPattern")] [SerializeField] protected BossPatternData primaryPattern; [Tooltip("기동 또는 거리 징벌 패턴")] [FormerlySerializedAs("leapPattern")] [SerializeField] protected BossPatternData mobilityPattern; [Tooltip("비주 대상 원거리 견제 패턴")] [SerializeField] protected BossPatternData utilityPattern; [Tooltip("Phase 3 조합 패턴")] [SerializeField] protected BossPatternData comboPattern; [Tooltip("특정 상황에서 우선 발동하는 징벌 패턴")] [FormerlySerializedAs("downPunishPattern")] [SerializeField] protected BossPatternData punishPattern; [Tooltip("파티 누킹을 시험하는 시그니처 패턴")] [SerializeField] protected BossPatternData signaturePattern; [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; [Tooltip("원거리 견제 패턴을 고려하기 시작하는 최소 거리")] [Min(0f)] [SerializeField] protected float utilityTriggerDistance = 5f; [Header("Pattern Flow")] [Tooltip("대형 패턴(시그니처/기동/조합) 직후 기본 패턴 최소 순환 횟수")] [Min(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2; [Header("시그니처 효과 설정")] [Tooltip("시그니처 패턴 차단에 필요한 누적 피해 비율")] [Range(0f, 1f)] [SerializeField] protected float signatureRequiredDamageRatio = 0.1f; [Tooltip("시그니처 준비 상태를 나타내는 이상상태")] [SerializeField] protected AbnormalityData signatureTelegraphAbnormality; [Tooltip("시그니처 차단 성공 시 보스가 멈추는 시간")] [Min(0f)] [SerializeField] protected float signatureSuccessStaggerDuration = 2f; [Tooltip("시그니처 실패 시 모든 플레이어에게 적용할 디버프")] [SerializeField] protected AbnormalityData signatureFailureAbnormality; [Tooltip("시그니처 실패 시 모든 플레이어에게 주는 기본 피해")] [Min(0f)] [SerializeField] protected float signatureFailureDamage = 40f; [Tooltip("시그니처 실패 시 넉백이 적용되는 반경")] [Min(0f)] [SerializeField] protected float signatureFailureKnockbackRadius = 8f; [Tooltip("시그니처 실패 시 다운이 적용되는 반경")] [Min(0f)] [SerializeField] protected float signatureFailureDownRadius = 3f; [Tooltip("시그니처 실패 시 넉백 속도")] [Min(0f)] [SerializeField] protected float signatureFailureKnockbackSpeed = 12f; [Tooltip("시그니처 실패 시 넉백 지속 시간")] [Min(0f)] [SerializeField] protected float signatureFailureKnockbackDuration = 0.35f; [Tooltip("시그니처 실패 시 다운 지속 시간")] [Min(0f)] [SerializeField] protected float signatureFailureDownDuration = 2f; [Header("Behavior")] [Tooltip("true면 컨텍스트 코드가 AI를 직접 구동합니다. false면 BehaviorGraph가 모든 의사결정을 담당합니다.")] [SerializeField] protected bool disableBehaviorGraph = false; [Tooltip("디버그 로그 출력 여부")] [SerializeField] protected bool debugMode = false; protected readonly Dictionary patternCooldownTracker = new Dictionary(); protected Coroutine activePatternCoroutine; protected GameObject currentTarget; protected float nextTargetRefreshTime; protected int meleePatternCounter; protected int basicLoopCountSinceLastBigPattern; /// /// 전용 컨텍스트 사용 시 BehaviorGraph를 비활성화할지 여부 /// public bool DisableBehaviorGraph => disableBehaviorGraph; /// /// 기동 패턴을 고려하는 최소 거리 /// public float MobilityTriggerDistance => mobilityTriggerDistance; /// /// 원거리 견제 패턴을 고려하는 최소 거리 /// public float UtilityTriggerDistance => utilityTriggerDistance; /// /// 징벌 패턴을 고려하는 최대 반경 /// public float PunishSearchRadius => punishSearchRadius; /// /// 현재 전투 대상 /// public GameObject CurrentTarget => currentTarget; /// /// EnemyBase 접근자 /// public EnemyBase EnemyBase => enemyBase; /// /// 현재 전투 기준이 되는 주 대상을 반환합니다. /// public GameObject ResolvePrimaryTarget() { if (IsValidHostileTarget(currentTarget)) return currentTarget; GameObject highestThreatTarget = enemyBase != null ? enemyBase.GetHighestThreatTarget(currentTarget, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity) : null; return highestThreatTarget != null ? highestThreatTarget : FindNearestLivingTarget(); } /// /// 디버그 로그 출력 여부 /// public bool DebugModeEnabled => debugMode; /// /// 시그니처 실패 시 모든 플레이어에게 주는 기본 피해 /// public float SignatureFailureDamage => signatureFailureDamage; /// /// 시그니처 실패 시 모든 플레이어에게 적용할 디버프 /// public AbnormalityData SignatureFailureAbnormality => signatureFailureAbnormality; /// /// 시그니처 실패 시 넉백이 적용되는 반경 /// public float SignatureFailureKnockbackRadius => signatureFailureKnockbackRadius; /// /// 시그니처 실패 시 다운이 적용되는 반경 /// public float SignatureFailureDownRadius => signatureFailureDownRadius; /// /// 시그니처 실패 시 넉백 속도 /// public float SignatureFailureKnockbackSpeed => signatureFailureKnockbackSpeed; /// /// 시그니처 실패 시 넉백 지속 시간 /// public float SignatureFailureKnockbackDuration => signatureFailureKnockbackDuration; /// /// 시그니처 실패 시 다운 지속 시간 /// public float SignatureFailureDownDuration => signatureFailureDownDuration; /// /// 마지막 충전 차단 시 설정된 경직 시간 (BossPatternActionBase가 설정) /// public float LastChargeStaggerDuration { get; set; } /// /// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부 /// public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned; /// /// 현재 보스 패턴 페이즈 /// 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 (IsBehaviorSuppressed) { StopMovement(); return; } if (!disableBehaviorGraph) return; RefreshTargetIfNeeded(); UpdateMovement(); if (skillController.IsPlayingAnimation) return; // 1. 다운 추가타 (최우선 인터럽트, grace period 면제) if (TryStartPunishPattern()) return; // 2. 조합 패턴 (Phase 3, 드물게) if (TryStartComboPattern()) return; // 4. 기동 패턴 (거리 기반 조건부) if (TryStartMobilityPattern()) return; // 5. 원거리 견제 (보조) if (TryStartUtilityPattern()) return; // 6. 기본 루프 TryStartPrimaryLoopPattern(); } /// /// 근접 패턴 사용 카운터를 갱신합니다. /// public void RegisterPatternUse(BossPatternData pattern) { if (pattern == null) return; if (pattern.IsMelee) { meleePatternCounter++; basicLoopCountSinceLastBigPattern++; } if (pattern.Category == PatternCategory.Punish || pattern.IsBigPattern) { basicLoopCountSinceLastBigPattern = 0; } } /// /// 살아 있는 적대 대상인지 확인합니다. /// 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 bool IsValidUtilityTarget(GameObject candidate) { if (!IsValidHostileTarget(candidate)) return false; if (candidate == ResolvePrimaryTarget()) return false; float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f; float distance = Vector3.Distance(transform.position, candidate.transform.position); return distance >= utilityTriggerDistance && distance <= maxDistance; } /// /// 현재 주 대상이 아닌 원거리 견제 대상을 찾습니다. /// public GameObject FindUtilityTarget() { PlayerNetworkController[] players = FindObjectsByType(FindObjectsSortMode.None); List validTargets = new List(); GameObject primaryTarget = ResolvePrimaryTarget(); 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 (!IsValidUtilityTarget(candidate)) continue; validTargets.Add(candidate); } if (validTargets.Count == 0) { if (IsValidHostileTarget(primaryTarget)) { float maxDistance = enemyBase != null && enemyBase.Data != null ? enemyBase.Data.AggroRange : 20f; float distance = Vector3.Distance(transform.position, primaryTarget.transform.position); if (distance >= utilityTriggerDistance && distance <= maxDistance) return primaryTarget; } return null; } int randomIndex = UnityEngine.Random.Range(0, validTargets.Count); return validTargets[randomIndex]; } /// /// 가장 가까운 생존 플레이어를 찾습니다. /// 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}"); } /// /// 모든 패턴 슬롯에 포함된 고유 스킬 목록을 반환합니다. 디버그 용도로 사용됩니다. /// public List GetAllPatternSkills() { HashSet skillSet = new HashSet(); BossPatternData[] allPatterns = { primaryPattern, mobilityPattern, utilityPattern, comboPattern, punishPattern, signaturePattern }; for (int i = 0; i < allPatterns.Length; i++) { BossPatternData pattern = allPatterns[i]; if (pattern?.Steps == null) continue; for (int j = 0; j < pattern.Steps.Count; j++) { PatternStep step = pattern.Steps[j]; if (step.Skill != null) skillSet.Add(step.Skill); } } return new List(skillSet); } /// /// 지정 패턴이 grace period를 통과했는지 반환합니다. /// Punish/Melee/Utility는 항상 허용됩니다. /// public bool IsPatternGracePeriodAllowed(BossPatternData pattern) { if (pattern == null) return false; if (pattern.Category == PatternCategory.Punish) return true; if (pattern.IsMelee || pattern.TargetMode == TargetResolveMode.Utility) return true; return basicLoopCountSinceLastBigPattern >= basicLoopMinCountAfterBigPattern; } /// /// 조합 패턴 사용 가능 여부를 반환합니다. /// public bool IsComboPatternReady() { if (!IsServer || bossEnemy == null || skillController == null) return false; if (IsBehaviorSuppressed) return false; if (activePatternCoroutine != null) return false; if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation) return false; if (!IsPatternGracePeriodAllowed(comboPattern)) return false; return IsPatternReady(comboPattern); } 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 = mobilityPattern; if (!IsPatternReady(pattern)) return false; GameObject target = FindMobilityTarget(); if (target == null) return false; currentTarget = target; StartPattern(pattern, target); return true; } protected virtual bool TryStartUtilityPattern() { BossPatternData pattern = utilityPattern; if (!IsPatternReady(pattern)) return false; GameObject target = FindUtilityTarget(); if (target == null) return false; currentTarget = target; StartPattern(pattern, target); return true; } /// /// 다운 대상이 존재하면 징벌 패턴을 발동합니다. /// protected virtual bool TryStartPunishPattern() { BossPatternData pattern = punishPattern; if (!IsPatternReady(pattern)) return false; HitReactionController[] hitReactionControllers = FindObjectsByType(FindObjectsSortMode.None); GameObject nearestDownedTarget = null; float nearestDistance = 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) continue; if (Team.IsSameTeam(gameObject, candidate)) continue; IDamageable damageable = candidate.GetComponent(); if (damageable != null && damageable.IsDead) continue; float distance = Vector3.Distance(transform.position, candidate.transform.position); if (distance > punishSearchRadius || distance >= nearestDistance) continue; nearestDistance = distance; nearestDownedTarget = candidate; } if (nearestDownedTarget == null) return false; currentTarget = nearestDownedTarget; StartPattern(pattern, nearestDownedTarget); return true; } /// /// Phase 3 조합 패턴을 발동합니다. /// protected virtual bool TryStartComboPattern() { if (!IsComboPatternReady()) return false; currentTarget = ResolvePrimaryTarget(); StartPattern(comboPattern, currentTarget); return true; } protected virtual BossPatternData SelectPrimaryLoopPattern() { if (!IsPatternReady(primaryPattern)) return null; meleePatternCounter++; return primaryPattern; } /// /// 기본 패턴을 선택하고 카운터를 갱신합니다. /// public BossPatternData SelectAndRegisterBasicLoopPattern() { if (!IsPatternReady(primaryPattern)) return null; RegisterPatternUse(primaryPattern); return primaryPattern; } /// /// 기본 패턴이 사용 가능한지 확인합니다. /// 상태 변경 없이 순수 검사만 수행합니다. /// public bool IsBasicLoopReady() { return IsPatternReady(primaryPattern); } protected virtual void StartPattern(BossPatternData pattern, GameObject target) { if (pattern == null || activePatternCoroutine != null) return; currentTarget = target; LogDebug(GetType().Name, $"패턴 시작: {pattern.PatternName} / Target={(target != null ? target.name : "None")} / Phase={CurrentPatternPhase}"); CombatBalanceTracker.RecordBossPattern(pattern.PatternName); 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) { GameObject jumpTarget = FindMobilityTarget(); if (jumpTarget == null) { LogDebug(GetType().Name, $"점프 대상 없음, 패턴 조기 종료: {pattern.PatternName}"); break; } target = jumpTarget; currentTarget = jumpTarget; enemyBase?.SetJumpTarget(jumpTarget.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 (abnormalityManager == null) abnormalityManager = GetComponent(); if (navMeshAgent == null) navMeshAgent = GetComponent(); if (behaviorGraphAgent == null) behaviorGraphAgent = GetComponent(); } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); } private static bool HasAnimatorParameter(Animator animator, string parameterName, AnimatorControllerParameterType parameterType) { if (animator == null || string.IsNullOrEmpty(parameterName)) return false; AnimatorControllerParameter[] parameters = animator.parameters; for (int i = 0; i < parameters.Length; i++) { AnimatorControllerParameter parameter = parameters[i]; if (parameter.type == parameterType && parameter.name == parameterName) return true; } return false; } } }