using System.Collections.Generic; using Colosseum.AI; using Colosseum.Abnormalities; using Colosseum.Player; using Colosseum.Skills; using Unity.Behavior; using Unity.Netcode; using UnityEngine; namespace Colosseum.Enemy { /// /// 마지막 패턴 실행 결과입니다. /// public enum BossPatternExecutionResult { None, Running, Succeeded, Failed, Cancelled, } /// /// 보스 BT가 읽고 쓰는 런타임 전투 상태를 보관합니다. /// 타겟, 페이즈 진행 상태, 패턴 쿨다운 같은 전투 런타임 결과를 외부 시스템과 공유합니다. /// [DisallowMultipleComponent] [RequireComponent(typeof(BossEnemy))] [RequireComponent(typeof(SkillController))] public class BossBehaviorRuntimeState : NetworkBehaviour { public static event System.Action PlayerRevivedBySkill; [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 Flow")] [Tooltip("패턴 하나가 끝난 뒤 다음 패턴을 시작하기까지의 공통 텀")] [Min(0f)] [SerializeField] protected float commonPatternInterval = 0.35f; [Tooltip("패턴 종료 후 Idle 자세가 잠깐 안착할 수 있도록 추가로 확보하는 시간")] [Min(0f)] [SerializeField] protected float postPatternIdleSettleDuration = 0.12f; [Header("Phase State")] [Tooltip("BT가 관리하는 최대 페이즈 수")] [Min(1)] [SerializeField] protected int maxPatternPhase = 3; [Tooltip("디버그 로그 출력 여부")] [SerializeField] protected bool debugMode = false; protected readonly Dictionary patternCooldownTracker = new Dictionary(); protected readonly Dictionary customPhaseConditions = new Dictionary(); protected GameObject currentTarget; protected int meleePatternCounter; protected int basicLoopCountSinceLastBigPattern; protected int currentPatternPhase = 1; protected float currentPhaseStartTime; protected float nextPatternReadyTime; protected float lastPatternCompletedTime = float.NegativeInfinity; protected BossPatternExecutionResult lastPatternExecutionResult; protected BossPatternData lastExecutedPattern; protected BossPatternData activePattern; protected bool currentPatternSkillStartsFromIdle; protected bool currentPatternSkillReturnsToIdle; protected GameObject lastReviveCaster; protected GameObject lastRevivedTarget; protected float lastReviveEventTime = float.NegativeInfinity; /// /// 현재 전투 대상 /// public GameObject CurrentTarget => currentTarget; /// /// BT가 관리하는 현재 페이즈 /// public int CurrentPatternPhase => Mathf.Clamp(currentPatternPhase, 1, Mathf.Max(1, maxPatternPhase)); /// /// BT가 관리하는 최대 페이즈 수 /// public int MaxPatternPhase => Mathf.Max(1, maxPatternPhase); /// /// 현재 페이즈의 경과 시간 /// public float PhaseElapsedTime => Time.time - currentPhaseStartTime; /// /// 마지막 대형/징벌 패턴 이후 누적된 기본 루프 횟수 /// public int BasicLoopCountSinceLastBigPattern => basicLoopCountSinceLastBigPattern; /// /// 패턴 종료 후 다음 패턴 시작까지 남은 공통 텀입니다. /// public float RemainingPatternInterval => Mathf.Max(0f, nextPatternReadyTime - Time.time); public float RemainingPatternIdleSettleTime => Mathf.Max(0f, (lastPatternCompletedTime + Mathf.Max(0f, postPatternIdleSettleDuration)) - Time.time); /// /// 마지막 패턴 실행 결과 /// public BossPatternExecutionResult LastPatternExecutionResult => lastPatternExecutionResult; /// /// 마지막으로 실행한 패턴 /// public BossPatternData LastExecutedPattern => lastExecutedPattern; /// /// 현재 패턴 실행 중인지 여부 /// public bool IsExecutingPattern => activePattern != null && lastPatternExecutionResult == BossPatternExecutionResult.Running; /// /// 현재 스킬 스텝이 패턴 시작에서 Idle과 이어져야 하는지 여부 /// public bool CurrentPatternSkillStartsFromIdle => currentPatternSkillStartsFromIdle; /// /// 현재 스킬 스텝이 패턴 종료에서 Idle로 돌아가야 하는지 여부 /// public bool CurrentPatternSkillReturnsToIdle => currentPatternSkillReturnsToIdle; /// /// EnemyBase 접근자 /// public EnemyBase EnemyBase => enemyBase; /// /// 디버그 로그 출력 여부 /// public bool DebugModeEnabled => debugMode; /// /// 마지막 충전 차단 시 설정된 경직 시간 (BossPatternActionBase가 설정) /// public float LastChargeStaggerDuration { get; set; } /// /// 마지막 패턴 실행에서 충전이 차단되었는지 여부. /// BT 노드(IsChargeBrokenCondition)에서 판독합니다. /// public bool WasChargeBroken { get; set; } /// /// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부 /// public bool IsBehaviorSuppressed => abnormalityManager != null && abnormalityManager.IsStunned; /// protected virtual void Awake() { ResolveReferences(); ResetPhaseState(); } public override void OnNetworkSpawn() { ResolveReferences(); ResetPhaseState(); if (!IsServer) { enabled = false; return; } PlayerRevivedBySkill += HandlePlayerRevivedBySkill; } protected virtual void Update() { if (!IsServer) return; ResolveReferences(); if (bossEnemy == null || enemyBase == null || skillController == null) return; if (bossEnemy.IsDead) return; if (IsBehaviorSuppressed) StopMovement(); } /// /// BT가 선택한 현재 전투 대상을 동기화합니다. /// public void SetCurrentTarget(GameObject target) { currentTarget = target; } /// /// BT가 현재 페이즈 값을 갱신합니다. /// 필요하면 경과 시간 기준도 함께 초기화합니다. /// public void SetCurrentPatternPhase(int phase, bool resetTimer = true) { currentPatternPhase = Mathf.Clamp(phase, 1, MaxPatternPhase); if (resetTimer) currentPhaseStartTime = Time.time; } /// /// 현재 페이즈 타이머를 다시 시작합니다. /// public void RestartCurrentPhaseTimer() { currentPhaseStartTime = Time.time; } /// /// 패턴 실행 시작을 기록합니다. /// public void BeginPatternExecution(BossPatternData pattern) { activePattern = pattern; lastExecutedPattern = pattern; lastPatternExecutionResult = BossPatternExecutionResult.Running; currentPatternSkillStartsFromIdle = false; currentPatternSkillReturnsToIdle = false; } /// /// 패턴 실행 결과를 기록합니다. /// public void CompletePatternExecution(BossPatternData pattern, BossPatternExecutionResult result) { lastExecutedPattern = pattern; lastPatternExecutionResult = result; activePattern = null; currentPatternSkillStartsFromIdle = false; currentPatternSkillReturnsToIdle = false; lastPatternCompletedTime = Time.time; if (pattern != null && IsTerminalPatternExecutionResult(result)) StartCommonPatternInterval(); } /// /// 현재 실행할 패턴 스킬이 패턴 시작/종료 경계인지 기록합니다. /// public void SetCurrentPatternSkillBoundary(bool startsFromIdle, bool returnsToIdle) { currentPatternSkillStartsFromIdle = startsFromIdle; currentPatternSkillReturnsToIdle = returnsToIdle; } /// /// 부활 스킬 사용 사실을 보스 AI에 알립니다. /// public static void ReportPlayerRevivedBySkill(GameObject caster, GameObject revivedTarget) { PlayerRevivedBySkill?.Invoke(caster, revivedTarget); } /// /// 최근 부활 트리거가 아직 유효한지 확인합니다. /// public bool HasRecentReviveTrigger(float maxAge) { return ResolveRecentReviveTriggerTarget(maxAge) != null; } /// /// 최근 부활 트리거에서 우선 공격할 대상을 반환합니다. /// public GameObject ResolveRecentReviveTriggerTarget(float maxAge, bool preferCaster = true, bool fallbackToRevivedTarget = true) { if (Time.time - lastReviveEventTime > Mathf.Max(0f, maxAge)) return null; GameObject preferredTarget = preferCaster ? lastReviveCaster : lastRevivedTarget; if (IsValidReviveTriggerTarget(preferredTarget)) return preferredTarget; if (!fallbackToRevivedTarget) return null; GameObject fallbackTarget = preferCaster ? lastRevivedTarget : lastReviveCaster; return IsValidReviveTriggerTarget(fallbackTarget) ? fallbackTarget : null; } /// /// 페이즈 커스텀 조건을 기록합니다. /// public void SetPhaseCustomCondition(string conditionId, bool value) { if (string.IsNullOrEmpty(conditionId)) return; customPhaseConditions[conditionId] = value; } /// /// 페이즈 커스텀 조건 값을 읽습니다. /// public bool CheckPhaseCustomCondition(string conditionId) { return !string.IsNullOrEmpty(conditionId) && customPhaseConditions.TryGetValue(conditionId, out bool value) && value; } public void IncrementBasicLoopCount(int count = 1) { int appliedCount = Mathf.Max(0, count); if (appliedCount <= 0) return; meleePatternCounter += appliedCount; basicLoopCountSinceLastBigPattern += appliedCount; } public void ResetBasicLoopCount() { basicLoopCountSinceLastBigPattern = 0; } /// /// 로그를 출력합니다. /// public void LogDebug(string source, string message) { if (debugMode) Debug.Log($"[{source}] {message}"); } public bool IsPatternReady(BossPatternData pattern) { if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0) return false; if (Time.time < lastPatternCompletedTime + Mathf.Max(0f, postPatternIdleSettleDuration)) return false; if (!IsCommonPatternIntervalReady()) return false; if (!patternCooldownTracker.TryGetValue(pattern, out float readyTime)) return true; return Time.time >= readyTime; } /// /// 패턴 쿨다운을 설정합니다. BT 노드(BossPatternActionBase)와 코드 폴백 모두에서 호출합니다. /// public void SetPatternCooldown(BossPatternData pattern) { if (pattern != null) patternCooldownTracker[pattern] = Time.time + pattern.Cooldown; } /// /// 공통 패턴 텀이 끝났는지 반환합니다. /// public bool IsCommonPatternIntervalReady() { return Time.time >= nextPatternReadyTime; } /// /// 현재 시점부터 공통 패턴 텀을 다시 시작합니다. /// public void StartCommonPatternInterval() { nextPatternReadyTime = Time.time + Mathf.Max(0f, commonPatternInterval); } 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 virtual void ResetPhaseState() { currentPatternPhase = 1; currentPhaseStartTime = Time.time; nextPatternReadyTime = 0f; lastPatternCompletedTime = float.NegativeInfinity; lastPatternExecutionResult = BossPatternExecutionResult.None; lastExecutedPattern = null; lastReviveCaster = null; lastRevivedTarget = null; lastReviveEventTime = float.NegativeInfinity; customPhaseConditions.Clear(); } private static bool IsTerminalPatternExecutionResult(BossPatternExecutionResult result) { return result == BossPatternExecutionResult.Succeeded || result == BossPatternExecutionResult.Failed || result == BossPatternExecutionResult.Cancelled; } public override void OnNetworkDespawn() { if (IsServer) PlayerRevivedBySkill -= HandlePlayerRevivedBySkill; base.OnNetworkDespawn(); } protected void HandlePlayerRevivedBySkill(GameObject caster, GameObject revivedTarget) { if (!IsServer) return; lastReviveCaster = caster; lastRevivedTarget = revivedTarget; lastReviveEventTime = Time.time; LogDebug(nameof(BossBehaviorRuntimeState), $"부활 트리거 기록: 시전자={caster?.name ?? "없음"} / 대상={revivedTarget?.name ?? "없음"}"); } protected bool IsValidReviveTriggerTarget(GameObject candidate) { if (candidate == null || !candidate.activeInHierarchy) return false; PlayerNetworkController player = candidate.GetComponent(); return player != null && !player.IsDead; } 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; } } }