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("slamPattern")] [SerializeField] protected BossPatternData secondaryPattern; [Tooltip("기동 또는 거리 징벌 패턴")] [FormerlySerializedAs("leapPattern")] [SerializeField] protected BossPatternData mobilityPattern; [Tooltip("비주 대상 원거리 견제 패턴")] [SerializeField] protected BossPatternData utilityPattern; [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 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("Signature Pattern")] [Tooltip("시그니처 패턴을 사용하기 시작하는 최소 페이즈")] [Min(1)] [SerializeField] protected int signatureMinPhase = 2; [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("전용 컨텍스트 사용 시 기존 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; protected bool isSignaturePatternActive; protected bool isPreviewingSignatureTelegraph; protected float signatureAccumulatedDamage; protected float signatureRequiredDamage; protected float signatureElapsedTime; protected float signatureTotalDuration; /// /// 전용 컨텍스트 사용 시 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 IsSignaturePatternActive => isSignaturePatternActive; public string SignaturePatternName => isSignaturePatternActive && signaturePattern != null ? signaturePattern.PatternName : string.Empty; public float SignatureAccumulatedDamage => signatureAccumulatedDamage; public float SignatureRequiredDamage => signatureRequiredDamage; public float SignatureBreakProgressNormalized => signatureRequiredDamage > 0f ? Mathf.Clamp01(signatureAccumulatedDamage / signatureRequiredDamage) : 0f; public float SignatureElapsedTime => signatureElapsedTime; public float SignatureTotalDuration => signatureTotalDuration; public float SignatureCastProgressNormalized => signatureTotalDuration > 0f ? Mathf.Clamp01(signatureElapsedTime / signatureTotalDuration) : 0f; public float SignatureRemainingTime => Mathf.Max(0f, signatureTotalDuration - signatureElapsedTime); /// /// 디버그 로그 출력 여부 /// public bool DebugModeEnabled => debugMode; /// /// 기절 등으로 인해 보스 전투 로직을 진행할 수 없는 상태인지 여부 /// public bool IsBehaviorSuppressed => isPreviewingSignatureTelegraph || (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; if (TryStartMobilityPattern()) return; if (TryStartUtilityPattern()) return; TryStartPrimaryLoopPattern(); } /// /// 현재 역할의 패턴 데이터를 반환합니다. /// public BossPatternData GetPattern(BossCombatPatternRole role) { return role switch { BossCombatPatternRole.Primary => primaryPattern, BossCombatPatternRole.Secondary => secondaryPattern, BossCombatPatternRole.Mobility => mobilityPattern, BossCombatPatternRole.Utility => utilityPattern, BossCombatPatternRole.Punish => punishPattern, BossCombatPatternRole.Signature => signaturePattern, _ => 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 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 bool IsSignaturePatternReady() { if (!IsServer || bossEnemy == null || skillController == null) return false; if (IsBehaviorSuppressed) return false; if (CurrentPatternPhase < signatureMinPhase) return false; if (activePatternCoroutine != null || isSignaturePatternActive) return false; if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation) return false; return UsePatternAction.IsPatternReady(gameObject, signaturePattern); } /// /// 시그니처 패턴을 시작합니다. /// public bool TryStartSignaturePattern(GameObject target) { if (!IsSignaturePatternReady()) return false; GameObject resolvedTarget = IsValidHostileTarget(target) ? target : FindNearestLivingTarget(); currentTarget = resolvedTarget; activePatternCoroutine = StartCoroutine(RunSignaturePatternCoroutine(signaturePattern, resolvedTarget)); return true; } /// /// 디버그 또는 특수 연출에서 시그니처 패턴을 강제로 시작합니다. /// public bool ForceStartSignaturePattern(GameObject target = null) { if (!IsServer || signaturePattern == null || activePatternCoroutine != null || isSignaturePatternActive) return false; GameObject resolvedTarget = IsValidHostileTarget(target) ? target : ResolvePrimaryTarget(); activePatternCoroutine = StartCoroutine(RunSignaturePatternCoroutine(signaturePattern, resolvedTarget)); return true; } /// /// 네트워크 상태와 무관하게 시그니처 전조 모션만 미리보기로 재생합니다. /// 전조 연출 확인용 디버그 경로입니다. /// public bool PreviewSignatureTelegraph() { if (signaturePattern == null || skillController == null) return false; if (activePatternCoroutine != null || isSignaturePatternActive || isPreviewingSignatureTelegraph) return false; StartCoroutine(PreviewSignatureTelegraphCoroutine()); return true; } 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 bool TryStartUtilityPattern() { BossPatternData pattern = GetPattern(BossCombatPatternRole.Utility); if (!IsPatternReady(pattern)) return false; GameObject target = FindUtilityTarget(); 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; 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 && 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 (abnormalityManager == null) abnormalityManager = GetComponent(); if (navMeshAgent == null) navMeshAgent = GetComponent(); if (behaviorGraphAgent == null) behaviorGraphAgent = GetComponent(); if (enemyBase != null) { enemyBase.OnDamageTaken -= HandleBossDamageTaken; enemyBase.OnDamageTaken += HandleBossDamageTaken; } } public override void OnNetworkDespawn() { if (enemyBase != null) { enemyBase.OnDamageTaken -= HandleBossDamageTaken; } base.OnNetworkDespawn(); } private IEnumerator RunSignaturePatternCoroutine(BossPatternData pattern, GameObject target) { StopMovement(); isSignaturePatternActive = true; signatureAccumulatedDamage = 0f; signatureRequiredDamage = bossEnemy.MaxHealth * signatureRequiredDamageRatio; signatureElapsedTime = 0f; signatureTotalDuration = CalculatePatternDuration(pattern); bool interrupted = false; bool completed = true; for (int i = 0; i < pattern.Steps.Count; i++) { if (HasMetSignatureBreakThreshold()) { interrupted = true; break; } PatternStep step = pattern.Steps[i]; if (step.Type == PatternStepType.Wait) { float remaining = step.Duration; while (remaining > 0f) { if (HasMetSignatureBreakThreshold()) { interrupted = true; break; } if (bossEnemy == null || bossEnemy.IsDead) { completed = false; break; } signatureElapsedTime += Time.deltaTime; remaining -= Time.deltaTime; yield return null; } if (interrupted || !completed) break; continue; } if (step.Skill == null) { completed = false; Debug.LogWarning($"[{GetType().Name}] 시그니처 패턴 스텝 스킬이 비어 있습니다. Pattern={pattern.PatternName}, Index={i}"); break; } if (!skillController.ExecuteSkill(step.Skill)) { completed = false; LogDebug(GetType().Name, $"시그니처 스킬 실행 실패: {step.Skill.SkillName}"); break; } while (skillController != null && skillController.IsPlayingAnimation) { if (HasMetSignatureBreakThreshold()) { interrupted = true; break; } if (bossEnemy == null || bossEnemy.IsDead) { completed = false; break; } signatureElapsedTime += Time.deltaTime; yield return null; } if (interrupted || !completed) break; } if (interrupted) { skillController?.CancelSkill(SkillCancelReason.Interrupt); UsePatternAction.MarkPatternUsed(gameObject, pattern); LogDebug(GetType().Name, $"시그니처 차단 성공: 누적 피해 {signatureAccumulatedDamage:F1} / 필요 {signatureRequiredDamage:F1}"); CombatBalanceTracker.RecordBossEvent("집행 개시 차단 성공"); if (signatureSuccessStaggerDuration > 0f) { if (enemyBase != null && enemyBase.Animator != null && HasAnimatorParameter(enemyBase.Animator, "Hit", AnimatorControllerParameterType.Trigger)) { enemyBase.Animator.SetTrigger("Hit"); } float endTime = Time.time + signatureSuccessStaggerDuration; while (Time.time < endTime && bossEnemy != null && !bossEnemy.IsDead) { StopMovement(); yield return null; } } } else if (completed) { UsePatternAction.MarkPatternUsed(gameObject, pattern); LogDebug(GetType().Name, $"시그니처 실패: 누적 피해 {signatureAccumulatedDamage:F1} / 필요 {signatureRequiredDamage:F1}"); CombatBalanceTracker.RecordBossEvent("집행 개시 실패"); ExecuteSignatureFailure(); } if (abnormalityManager != null && signatureTelegraphAbnormality != null) { abnormalityManager.RemoveAbnormality(signatureTelegraphAbnormality); } isSignaturePatternActive = false; signatureAccumulatedDamage = 0f; signatureRequiredDamage = 0f; signatureElapsedTime = 0f; signatureTotalDuration = 0f; activePatternCoroutine = null; } private IEnumerator PreviewSignatureTelegraphCoroutine() { bool restoreBehaviorGraph = behaviorGraphAgent != null && behaviorGraphAgent.enabled; isPreviewingSignatureTelegraph = true; if (restoreBehaviorGraph) { behaviorGraphAgent.enabled = false; } StopMovement(); if (skillController != null && skillController.IsPlayingAnimation) { skillController.CancelSkill(SkillCancelReason.Interrupt); yield return null; } bool executed = false; for (int i = 0; i < signaturePattern.Steps.Count; i++) { PatternStep step = signaturePattern.Steps[i]; if (step == null || step.Type != PatternStepType.Skill || step.Skill == null) continue; executed = skillController.ExecuteSkill(step.Skill); break; } if (executed) { while (skillController != null && skillController.IsPlayingAnimation) { yield return null; } } if (abnormalityManager != null && signatureTelegraphAbnormality != null) { abnormalityManager.RemoveAbnormality(signatureTelegraphAbnormality); } if (restoreBehaviorGraph && behaviorGraphAgent != null) { behaviorGraphAgent.enabled = true; } isPreviewingSignatureTelegraph = false; } private static float CalculatePatternDuration(BossPatternData pattern) { if (pattern == null || pattern.Steps == null) return 0f; float totalDuration = 0f; for (int i = 0; i < pattern.Steps.Count; i++) { PatternStep step = pattern.Steps[i]; if (step == null) continue; if (step.Type == PatternStepType.Wait) { totalDuration += Mathf.Max(0f, step.Duration); continue; } if (step.Skill == null) continue; AnimationClip skillClip = step.Skill.SkillClip; if (skillClip != null) { float animationSpeed = Mathf.Max(0.01f, step.Skill.AnimationSpeed); totalDuration += skillClip.length / animationSpeed; } if (step.Skill.EndClip != null) { totalDuration += step.Skill.EndClip.length; } } return totalDuration; } private void ExecuteSignatureFailure() { PlayerNetworkController[] players = FindObjectsByType(FindObjectsSortMode.None); for (int i = 0; i < players.Length; i++) { PlayerNetworkController player = players[i]; if (player == null || player.IsDead || !player.gameObject.activeInHierarchy) continue; GameObject target = player.gameObject; if (!IsValidHostileTarget(target)) continue; player.TakeDamage(signatureFailureDamage, gameObject); AbnormalityManager abnormalityManager = target.GetComponent(); if (abnormalityManager != null && signatureFailureAbnormality != null) { abnormalityManager.ApplyAbnormality(signatureFailureAbnormality, gameObject); } HitReactionController hitReactionController = target.GetComponent(); if (hitReactionController == null) continue; float distance = Vector3.Distance(transform.position, target.transform.position); if (distance <= signatureFailureDownRadius) { hitReactionController.ApplyDown(signatureFailureDownDuration); continue; } if (distance > signatureFailureKnockbackRadius) continue; Vector3 knockbackDirection = target.transform.position - transform.position; knockbackDirection.y = 0f; if (knockbackDirection.sqrMagnitude < 0.0001f) { knockbackDirection = transform.forward; } hitReactionController.ApplyKnockback(knockbackDirection.normalized * signatureFailureKnockbackSpeed, signatureFailureKnockbackDuration); } } private bool HasMetSignatureBreakThreshold() { if (!isSignaturePatternActive) return false; if (signatureRequiredDamage <= 0f) return true; return signatureAccumulatedDamage >= signatureRequiredDamage; } 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; } private void HandleBossDamageTaken(float damage) { if (!IsServer || !isSignaturePatternActive || damage <= 0f) return; signatureAccumulatedDamage += damage; } } }