using System.Collections.Generic; using Colosseum.AI; using Colosseum.Abnormalities; using Colosseum.Player; using Colosseum.Skills; using Unity.Behavior; using Unity.Netcode; using UnityEngine; using UnityEngine.Serialization; 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(0)] [SerializeField] protected int basicLoopMinCountAfterBigPattern = 2; [Tooltip("패턴 하나가 끝난 뒤 다음 패턴을 시작하기까지의 공통 텀")] [Min(0f)] [SerializeField] protected float commonPatternInterval = 0.35f; [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("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 BossPatternExecutionResult lastPatternExecutionResult; protected BossPatternData lastExecutedPattern; 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 BossPatternExecutionResult LastPatternExecutionResult => lastPatternExecutionResult; /// /// 마지막으로 실행한 패턴 /// public BossPatternData LastExecutedPattern => lastExecutedPattern; /// /// EnemyBase 접근자 /// public EnemyBase EnemyBase => enemyBase; /// /// 디버그 로그 출력 여부 /// 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; } /// /// 마지막 패턴 실행에서 충전이 차단되었는지 여부. /// 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) { lastExecutedPattern = pattern; lastPatternExecutionResult = BossPatternExecutionResult.Running; } /// /// 패턴 실행 결과를 기록합니다. /// public void CompletePatternExecution(BossPatternData pattern, BossPatternExecutionResult result) { lastExecutedPattern = pattern; lastPatternExecutionResult = result; if (pattern != null && IsTerminalPatternExecutionResult(result)) StartCommonPatternInterval(); } /// /// 부활 스킬 사용 사실을 보스 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 RegisterPatternUse(BossPatternData pattern) { if (pattern == null) return; if (pattern.IsMelee) { meleePatternCounter++; basicLoopCountSinceLastBigPattern++; } if (pattern.Category == PatternCategory.Punish || pattern.IsBigPattern) { basicLoopCountSinceLastBigPattern = 0; } } /// /// 로그를 출력합니다. /// public void LogDebug(string source, string message) { if (debugMode) Debug.Log($"[{source}] {message}"); } /// /// 지정 패턴이 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 IsPatternReady(BossPatternData pattern) { if (pattern == null || pattern.Steps == null || pattern.Steps.Count == 0) 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; 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; } } }