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("Signature Pattern")] [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 bool isSignaturePatternActive; protected bool isPreviewingSignatureTelegraph; protected float signatureAccumulatedDamage; protected float signatureRequiredDamage; protected float signatureElapsedTime; protected float signatureTotalDuration; 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 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; // 1. 다운 추가타 (최우선 인터럽트, grace period 면제) if (TryStartPunishPattern()) return; // 2. 집행 개시 (Phase 3 시그니처) if (TryStartSignaturePatternInLoop()) return; // 3. 조합 패턴 (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 bool IsSignaturePatternReady() { if (!IsServer || bossEnemy == null || skillController == null) return false; if (IsBehaviorSuppressed) return false; if (activePatternCoroutine != null || isSignaturePatternActive) return false; if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation) return false; if (!IsPatternGracePeriodAllowed(signaturePattern)) return false; return IsPatternReady(signaturePattern); } /// /// 지정 패턴이 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 || isSignaturePatternActive) return false; if (bossEnemy.IsDead || bossEnemy.IsTransitioning || skillController.IsPlayingAnimation) return false; if (!IsPatternGracePeriodAllowed(comboPattern)) return false; return IsPatternReady(comboPattern); } /// /// 시그니처 패턴을 시작합니다. /// 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 = 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; } /// /// 시그니처 패턴을 context 루프에서 발동합니다. /// grace period와 Phase 제한을 적용합니다. /// protected virtual bool TryStartSignaturePatternInLoop() { if (!IsSignaturePatternReady()) return false; if (!IsPatternGracePeriodAllowed(signaturePattern)) return false; GameObject target = ResolvePrimaryTarget(); return TryStartSignaturePattern(target); } /// /// 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(); 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; } } }