using System; using Colosseum.Abnormalities; using Colosseum.AI; using Colosseum.Combat; using Colosseum.Enemy; using Colosseum.Player; using Colosseum.Skills; using Unity.Behavior; using Unity.Properties; using UnityEngine; using Action = Unity.Behavior.Action; /// /// 보스 공통 패턴 실행용 Behavior Action 기반 클래스입니다. /// [Serializable, GeneratePropertyBag] public abstract partial class BossPatternActionBase : Action { [SerializeReference] public BlackboardVariable Target; protected BossEnemy bossEnemy; protected EnemyBase enemyBase; protected SkillController skillController; protected BossCombatBehaviorContext combatBehaviorContext; protected UnityEngine.AI.NavMeshAgent navMeshAgent; protected AbnormalityManager abnormalityManager; private BossPatternData activePattern; private GameObject activeTarget; private int currentStepIndex; private bool isWaiting; private float waitEndTime; private bool isChargeWaiting; private float chargeEndTime; private float chargeAccumulatedDamage; private float chargeRequiredDamage; private ChargeStepData activeChargeData; private bool chargeTelegraphApplied; /// /// 액션 시작 시 실제로 실행할 패턴과 대상을 결정합니다. /// protected abstract bool TryResolvePattern(out BossPatternData pattern, out GameObject target); protected override Status OnStart() { ResolveReferences(); ClearRuntimeState(); if (!IsReady()) return Status.Failure; if (combatBehaviorContext.IsBehaviorSuppressed) { StopMovement(); return Status.Failure; } if (bossEnemy.IsDead || bossEnemy.IsTransitioning) return Status.Failure; if (skillController.IsPlayingAnimation) return Status.Failure; if (!TryResolvePattern(out BossPatternData pattern, out GameObject target)) return Status.Failure; activePattern = pattern; activeTarget = target; if (Target != null) Target.Value = target; StopMovement(); return ExecuteCurrentStep(); } protected override Status OnUpdate() { if (!IsReady() || activePattern == null) return Status.Failure; if (combatBehaviorContext.IsBehaviorSuppressed) { StopMovement(); return Status.Failure; } if (bossEnemy.IsDead || bossEnemy.IsTransitioning) return Status.Failure; if (isChargeWaiting) { if (chargeAccumulatedDamage >= chargeRequiredDamage) { EndChargeWait(broken: true); skillController?.CancelSkill(SkillCancelReason.Interrupt); LogDebug($"충전 차단 성공: 누적 {chargeAccumulatedDamage:F1} / 필요 {chargeRequiredDamage:F1}"); CombatBalanceTracker.RecordBossEvent("집행 개시 차단 성공"); return Status.Failure; } if (Time.time < chargeEndTime) return Status.Running; EndChargeWait(broken: false); } else if (isWaiting) { if (Time.time < waitEndTime) return Status.Running; isWaiting = false; } else if (skillController.IsPlayingAnimation) { return Status.Running; } currentStepIndex++; if (currentStepIndex >= activePattern.Steps.Count) { UsePatternAction.MarkPatternUsed(GameObject, activePattern); return Status.Success; } return ExecuteCurrentStep(); } protected override void OnEnd() { ClearRuntimeState(); } protected virtual GameObject ResolveStepTarget(GameObject fallbackTarget) { return fallbackTarget; } protected GameObject FindNearestLivingPlayer() { PlayerNetworkController[] players = UnityEngine.Object.FindObjectsByType(FindObjectsSortMode.None); GameObject nearestTarget = null; float nearestDistance = float.MaxValue; float maxDistance = 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; float distance = Vector3.Distance(GameObject.transform.position, candidate.transform.position); if (distance > maxDistance || distance >= nearestDistance) continue; nearestDistance = distance; nearestTarget = candidate; } return nearestTarget; } protected GameObject ResolvePrimaryTarget() { GameObject highestThreatTarget = enemyBase != null ? enemyBase.GetHighestThreatTarget(Target?.Value, null, enemyBase.Data != null ? enemyBase.Data.AggroRange : Mathf.Infinity) : null; GameObject target = highestThreatTarget != null ? highestThreatTarget : FindNearestLivingPlayer(); if (Target != null) Target.Value = target; return target; } protected bool IsValidHostileTarget(GameObject candidate) { if (candidate == null || !candidate.activeInHierarchy) return false; // 보스는 항상 적 팀이므로, 플레이어만 적대 대상으로 간주 if (candidate.GetComponent() == null) return false; IDamageable damageable = candidate.GetComponent(); return damageable == null || !damageable.IsDead; } protected void StopMovement() { if (navMeshAgent == null || !navMeshAgent.enabled) return; navMeshAgent.isStopped = true; navMeshAgent.ResetPath(); } protected void LogDebug(string message) { combatBehaviorContext?.LogDebug(GetType().Name, message); } private Status ExecuteCurrentStep() { if (activePattern == null || currentStepIndex < 0 || currentStepIndex >= activePattern.Steps.Count) return Status.Failure; PatternStep step = activePattern.Steps[currentStepIndex]; if (step.Type == PatternStepType.Wait) { isWaiting = true; waitEndTime = Time.time + step.Duration; return Status.Running; } if (step.Type == PatternStepType.ChargeWait) { StartChargeWait(step); return Status.Running; } if (step.Skill == null) { Debug.LogWarning($"[{GetType().Name}] 스킬이 비어 있는 패턴 스텝입니다: {activePattern.PatternName} / Step={currentStepIndex}"); return Status.Failure; } GameObject skillTarget = activeTarget; if (step.Skill.JumpToTarget) { skillTarget = ResolveStepTarget(activeTarget); if (skillTarget == null) { if (activePattern != null && activePattern.SkipJumpStepOnNoTarget) { UsePatternAction.MarkPatternUsed(GameObject, activePattern); LogDebug($"점프 대상 없음, 조합 패턴 조기 종료: {activePattern.PatternName}"); return Status.Success; } LogDebug($"점프 타겟을 찾지 못해 실패: {activePattern.PatternName}"); return Status.Failure; } enemyBase?.SetJumpTarget(skillTarget.transform.position); } if (!skillController.ExecuteSkill(step.Skill)) { Debug.LogWarning($"[{GetType().Name}] 스킬 실행 실패: {step.Skill.SkillName}"); return Status.Failure; } LogDebug($"패턴 실행: {activePattern.PatternName} / Step={currentStepIndex} / Skill={step.Skill.SkillName}"); return Status.Running; } /// /// 충전 대기를 시작합니다. 전조 이상상태를 부여하고 데미지 추적을 시작합니다. /// private void StartChargeWait(PatternStep step) { isChargeWaiting = true; activeChargeData = step.ChargeData; chargeEndTime = Time.time + step.Duration; chargeAccumulatedDamage = 0f; float damageRatio = activeChargeData != null ? activeChargeData.RequiredDamageRatio : 0.1f; chargeRequiredDamage = bossEnemy.MaxHealth * damageRatio; chargeTelegraphApplied = false; if (enemyBase != null) enemyBase.OnDamageTaken += OnChargeDamageTaken; if (activeChargeData != null && activeChargeData.TelegraphAbnormality != null && abnormalityManager != null) { abnormalityManager.ApplyAbnormality(activeChargeData.TelegraphAbnormality, GameObject); chargeTelegraphApplied = true; } LogDebug($"충전 대기 시작: 필요 피해={chargeRequiredDamage:F1} / 대기={step.Duration:F1}s"); } /// /// 충전 대기를 종료합니다. 전조 이상상태를 제거하고 데미지 추적을 해제합니다. /// private void EndChargeWait(bool broken) { isChargeWaiting = false; if (enemyBase != null) enemyBase.OnDamageTaken -= OnChargeDamageTaken; if (chargeTelegraphApplied && abnormalityManager != null && activeChargeData != null && activeChargeData.TelegraphAbnormality != null) { abnormalityManager.RemoveAbnormality(activeChargeData.TelegraphAbnormality); chargeTelegraphApplied = false; } if (broken && activeChargeData != null) { combatBehaviorContext.LastChargeStaggerDuration = activeChargeData.StaggerDuration; } activeChargeData = null; } /// /// 충전 중 보스가 받은 피해를 누적합니다. /// private void OnChargeDamageTaken(float damage) { if (!isChargeWaiting || damage <= 0f) return; chargeAccumulatedDamage += damage; } private bool IsReady() { return bossEnemy != null && enemyBase != null && skillController != null && combatBehaviorContext != null; } private void ResolveReferences() { if (bossEnemy == null) bossEnemy = GameObject.GetComponent(); if (enemyBase == null) enemyBase = GameObject.GetComponent(); if (skillController == null) skillController = GameObject.GetComponent(); if (combatBehaviorContext == null) combatBehaviorContext = GameObject.GetComponent(); if (navMeshAgent == null) navMeshAgent = GameObject.GetComponent(); if (abnormalityManager == null) abnormalityManager = GameObject.GetComponent(); } private void ClearRuntimeState() { if (isChargeWaiting) EndChargeWait(broken: false); activePattern = null; activeTarget = null; currentStepIndex = 0; isWaiting = false; waitEndTime = 0f; } }