using UnityEngine; using System.Collections.Generic; using Unity.Netcode; using Colosseum.Abnormalities; namespace Colosseum.Skills { /// /// 스킬 강제 취소 이유 /// public enum SkillCancelReason { None, Manual, Interrupt, Death, Stun, HitReaction, Respawn, Revive, } /// /// 스킬 실행을 관리하는 컴포넌트. /// 애니메이션 이벤트 기반으로 효과가 발동됩니다. /// public class SkillController : NetworkBehaviour { private const string SKILL_STATE_NAME = "Skill"; private const string END_STATE_NAME = "SkillEnd"; [Header("애니메이션")] [SerializeField] private Animator animator; [Tooltip("기본 Animator Controller (스킬 종료 후 복원용)")] [SerializeField] private RuntimeAnimatorController baseController; [Tooltip("Skill 상태에 연결된 기본 클립 (Override용)")] [SerializeField] private AnimationClip baseSkillClip; [Header("네트워크 동기화")] [Tooltip("이 SkillController가 사용하는 모든 스킬/엔드 클립 (순서대로 인덱스 부여). 서버→클라이언트 클립 동기화에 사용됩니다.")] [SerializeField] private List registeredClips = new(); [Header("설정")] [SerializeField] private bool debugMode = false; [Tooltip("공격 범위 시각화 (Scene 뷰에서 확인)")] [SerializeField] private bool showAreaDebug = true; [Tooltip("범위 표시 지속 시간")] [Min(0.1f)] [SerializeField] private float debugDrawDuration = 1f; [Header("디버그")] [Tooltip("마지막으로 강제 취소된 스킬 이름")] [SerializeField] private string lastCancelledSkillName = string.Empty; [Tooltip("마지막 강제 취소 이유")] [SerializeField] private SkillCancelReason lastCancelReason = SkillCancelReason.None; // 현재 실행 중인 스킬 private SkillData currentSkill; private SkillLoadoutEntry currentLoadoutEntry; private readonly List currentCastStartEffects = new(); private readonly Dictionary> currentTriggeredEffects = new(); private readonly List currentCastStartAbnormalities = new(); private readonly Dictionary> currentTriggeredAbnormalities = new(); private readonly List currentTriggeredTargetsBuffer = new(); private bool waitingForEndAnimation; // EndAnimation 종료 대기 중 private int currentRepeatCount = 1; private int currentIterationIndex = 0; private GameObject currentTargetOverride; // 쿨타임 추적 private Dictionary cooldownTracker = new Dictionary(); public bool IsExecutingSkill => currentSkill != null; public bool IsPlayingAnimation => currentSkill != null; public bool IsInEndAnimation => waitingForEndAnimation; public bool UsesRootMotion => currentSkill != null && currentSkill.UseRootMotion; public bool IgnoreRootMotionY => currentSkill != null && currentSkill.IgnoreRootMotionY; public SkillData CurrentSkill => currentSkill; public SkillLoadoutEntry CurrentLoadoutEntry => currentLoadoutEntry; public Animator Animator => animator; public SkillCancelReason LastCancelReason => lastCancelReason; public string LastCancelledSkillName => lastCancelledSkillName; public GameObject CurrentTargetOverride => currentTargetOverride; private void Awake() { lastCancelledSkillName = string.Empty; lastCancelReason = SkillCancelReason.None; if (animator == null) { animator = GetComponentInChildren(); } // 기본 컨트롤러 저장 if (baseController == null && animator != null) { baseController = animator.runtimeAnimatorController; } } private void Update() { if (currentSkill == null || animator == null) return; var stateInfo = animator.GetCurrentAnimatorStateInfo(0); // EndAnimation 종료 감지 if (waitingForEndAnimation) { if (stateInfo.normalizedTime >= 1f) { if (debugMode) Debug.Log($"[Skill] EndAnimation complete: {currentSkill.SkillName}"); RestoreBaseController(); ClearCurrentSkillState(); } return; } // 애니메이션 종료 시 처리 (OnSkillEnd 여부와 관계없이 애니메이션 끝까지 재생) if (stateInfo.normalizedTime >= 1f) { if (TryStartNextIteration()) return; if (currentSkill.EndClip != null) { // EndAnimation 재생 후 종료 대기 if (debugMode) Debug.Log($"[Skill] SkillAnimation done, playing EndAnimation: {currentSkill.SkillName}"); PlayEndClip(currentSkill.EndClip); waitingForEndAnimation = true; } else { // EndAnimation 없으면 바로 종료 if (debugMode) Debug.Log($"[Skill] Animation complete: {currentSkill.SkillName}"); RestoreBaseController(); ClearCurrentSkillState(); } } } /// /// 스킬 시전 /// public bool ExecuteSkill(SkillData skill) { return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill)); } /// /// 슬롯 엔트리 기준으로 스킬 시전 /// public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry) { currentTargetOverride = null; return ExecuteSkillInternal(loadoutEntry); } /// /// 타겟 오버라이드와 함께 스킬 시전. /// SingleAlly 타입 효과에서 외부 타겟을 사용할 때 호출합니다. /// public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry, GameObject targetOverride) { currentTargetOverride = targetOverride; return ExecuteSkillInternal(loadoutEntry); } /// /// 스킬 시전 공통 로직 /// private bool ExecuteSkillInternal(SkillLoadoutEntry loadoutEntry) { SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null; if (skill == null) { Debug.LogWarning("Skill is null!"); return false; } // 사망 상태면 스킬 사용 불가 var damageable = GetComponent(); if (damageable != null && damageable.IsDead) { if (debugMode) Debug.Log($"[Skill] Cannot execute skill - owner is dead"); return false; } if (IsExecutingSkill) { if (debugMode) Debug.Log($"Already executing skill: {currentSkill.SkillName}"); return false; } if (IsOnCooldown(skill)) { if (debugMode) Debug.Log($"Skill {skill.SkillName} is on cooldown"); return false; } currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill); currentSkill = skill; waitingForEndAnimation = false; lastCancelReason = SkillCancelReason.None; BuildResolvedEffects(currentLoadoutEntry); currentRepeatCount = currentLoadoutEntry.GetResolvedRepeatCount(); currentIterationIndex = 0; if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}"); // 쿨타임 시작 StartCooldown(skill, currentLoadoutEntry.GetResolvedCooldown()); StartCurrentIteration(); return true; } /// /// 시전 시작 즉시 발동하는 효과를 실행합니다. /// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다. /// private void TriggerCastStartEffects() { if (currentSkill == null) return; if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return; for (int i = 0; i < currentCastStartEffects.Count; i++) { SkillEffect effect = currentCastStartEffects[i]; if (effect == null) continue; if (debugMode) Debug.Log($"[Skill] Cast start effect: {effect.name} (index {i})"); effect.ExecuteOnCast(gameObject, currentTargetOverride); } if (currentCastStartAbnormalities.Count <= 0) return; AbnormalityManager abnormalityManager = GetComponent(); if (abnormalityManager == null) { if (debugMode) Debug.LogWarning("[Skill] Cast start abnormality skipped - no AbnormalityManager"); return; } for (int i = 0; i < currentCastStartAbnormalities.Count; i++) { AbnormalityData abnormality = currentCastStartAbnormalities[i]; if (abnormality == null) continue; if (debugMode) Debug.Log($"[Skill] Cast start abnormality: {abnormality.abnormalityName} (index {i})"); abnormalityManager.ApplyAbnormality(abnormality, gameObject); } } /// /// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다. /// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다. /// private void TriggerImmediateSelfEffectsIfNeeded() { if (currentSkill == null || currentTriggeredEffects.Count == 0) return; if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return; if (currentSkill.SkillClip != null && currentSkill.SkillClip.events != null && currentSkill.SkillClip.events.Length > 0) return; if (!currentTriggeredEffects.TryGetValue(0, out List effectsAtZero)) return; for (int i = 0; i < effectsAtZero.Count; i++) { SkillEffect effect = effectsAtZero[i]; if (effect == null || effect.TargetType != TargetType.Self) continue; if (debugMode) Debug.Log($"[Skill] Immediate self effect: {effect.name} (index {i})"); effect.ExecuteOnCast(gameObject, currentTargetOverride); } } /// /// 현재 슬롯 엔트리 기준으로 시전 시작/트리거 효과를 합성합니다. /// private void BuildResolvedEffects(SkillLoadoutEntry loadoutEntry) { currentCastStartEffects.Clear(); currentTriggeredEffects.Clear(); currentCastStartAbnormalities.Clear(); currentTriggeredAbnormalities.Clear(); if (loadoutEntry == null) return; loadoutEntry.CollectCastStartEffects(currentCastStartEffects); loadoutEntry.CollectTriggeredEffects(currentTriggeredEffects); loadoutEntry.CollectCastStartAbnormalities(currentCastStartAbnormalities); loadoutEntry.CollectTriggeredAbnormalities(currentTriggeredAbnormalities); } /// /// 현재 스킬의 반복 차수 하나를 시작합니다. /// private void StartCurrentIteration() { if (currentSkill == null) return; currentIterationIndex++; waitingForEndAnimation = false; if (debugMode && currentRepeatCount > 1) { Debug.Log($"[Skill] Iteration {currentIterationIndex}/{currentRepeatCount}: {currentSkill.SkillName}"); } TriggerCastStartEffects(); if (currentSkill.SkillClip != null && animator != null) { float resolvedAnimationSpeed = currentLoadoutEntry != null ? currentLoadoutEntry.GetResolvedAnimationSpeed() : currentSkill.AnimationSpeed; animator.speed = resolvedAnimationSpeed; PlaySkillClip(currentSkill.SkillClip); } TriggerImmediateSelfEffectsIfNeeded(); } /// /// 반복 시전이 남아 있으면 다음 차수를 시작합니다. /// private bool TryStartNextIteration() { if (currentSkill == null) return false; if (currentIterationIndex >= currentRepeatCount) return false; StartCurrentIteration(); return true; } /// /// 스킬 클립으로 Override Controller 생성 후 재생 /// private void PlaySkillClip(AnimationClip clip) { if (baseSkillClip == null) { Debug.LogError("[SkillController] Base Skill Clip is not assigned!"); return; } if (debugMode) { Debug.Log($"[Skill] PlaySkillClip: {clip.name}, BaseClip: {baseSkillClip.name}"); } var overrideController = new AnimatorOverrideController(baseController); overrideController[baseSkillClip] = clip; animator.runtimeAnimatorController = overrideController; // 애니메이터 완전 리셋 후 재생 animator.Rebind(); animator.Update(0f); animator.Play(SKILL_STATE_NAME, 0, 0f); // 클라이언트에 클립 동기화 if (IsServer && IsSpawned) PlaySkillClipClientRpc(registeredClips.IndexOf(clip)); } /// /// 종료 클립 재생 /// private void PlayEndClip(AnimationClip clip) { if (baseSkillClip == null) { Debug.LogError("[SkillController] Base Skill Clip is not assigned!"); return; } var overrideController = new AnimatorOverrideController(baseController); overrideController[baseSkillClip] = clip; animator.runtimeAnimatorController = overrideController; // 애니메이터 완전 리셋 후 재생 animator.Rebind(); animator.Update(0f); animator.Play(SKILL_STATE_NAME, 0, 0f); // 클라이언트에 클립 동기화 if (IsServer && IsSpawned) PlaySkillClipClientRpc(registeredClips.IndexOf(clip)); } /// /// 기본 컨트롤러로 복원 /// private void RestoreBaseController() { if (animator != null && baseController != null) { animator.runtimeAnimatorController = baseController; animator.speed = 1f; } // 클라이언트에 복원 동기화 if (IsServer && IsSpawned) RestoreBaseControllerClientRpc(); } /// /// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결) /// [Rpc(SendTo.NotServer)] private void PlaySkillClipClientRpc(int clipIndex) { if (baseSkillClip == null || animator == null || baseController == null) return; if (clipIndex < 0 || clipIndex >= registeredClips.Count || registeredClips[clipIndex] == null) { if (debugMode) Debug.LogWarning($"[SkillController] Clip index {clipIndex} not found in registeredClips. Add it to sync to clients."); return; } var overrideController = new AnimatorOverrideController(baseController); overrideController[baseSkillClip] = registeredClips[clipIndex]; animator.runtimeAnimatorController = overrideController; animator.Rebind(); animator.Update(0f); animator.Play(SKILL_STATE_NAME, 0, 0f); } /// /// 클라이언트: 기본 컨트롤러 복원 /// [Rpc(SendTo.NotServer)] private void RestoreBaseControllerClientRpc() { if (animator != null && baseController != null) animator.runtimeAnimatorController = baseController; } /// /// 애니메이션 이벤트에서 호출. Effect 리스트의 index번째 효과를 발동합니다. /// Animation Event: Function = OnEffect, Int Parameter = effect index (0-based) /// public void OnEffect(int index) { if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return; if (currentSkill == null) { if (debugMode) Debug.LogWarning("[Effect] No skill executing"); return; } // 사망 상태면 효과 발동 중단 var damageable = GetComponent(); if (damageable != null && damageable.IsDead) { if (debugMode) Debug.Log($"[Effect] Cancelled - owner is dead"); return; } if (!currentTriggeredEffects.TryGetValue(index, out List effects) || effects == null || effects.Count == 0) { if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}"); return; } for (int i = 0; i < effects.Count; i++) { SkillEffect effect = effects[i]; if (effect == null) continue; if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})"); // 공격 범위 시각화 if (showAreaDebug) { effect.DrawDebugRange(gameObject, debugDrawDuration); } effect.ExecuteOnCast(gameObject, currentTargetOverride); } ApplyTriggeredAbnormalities(index, effects); } /// /// 애니메이션 이벤트에서 호출. 스킬 종료를 요청합니다. /// 애니메이션은 끝까지 재생된 후 종료됩니다. /// Animation Event: Function = OnSkillEnd /// public void OnSkillEnd() { if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return; if (currentSkill == null) { if (debugMode) Debug.LogWarning("[SkillEnd] No skill executing"); return; } if (debugMode) Debug.Log($"[Skill] End event received: {currentSkill.SkillName}"); } /// /// 현재 스킬을 강제 취소합니다. /// public bool CancelSkill(SkillCancelReason reason = SkillCancelReason.Manual) { if (currentSkill == null) return false; lastCancelledSkillName = currentSkill.SkillName; lastCancelReason = reason; Debug.Log($"[Skill] Cancelled: {currentSkill.SkillName} / reason={reason}"); RestoreBaseController(); ClearCurrentSkillState(); return true; } public bool IsOnCooldown(SkillData skill) { if (!cooldownTracker.ContainsKey(skill)) return false; return Time.time < cooldownTracker[skill]; } public float GetRemainingCooldown(SkillData skill) { if (!cooldownTracker.ContainsKey(skill)) return 0f; float remaining = cooldownTracker[skill] - Time.time; return Mathf.Max(0f, remaining); } private void StartCooldown(SkillData skill, float cooldownDuration) { cooldownTracker[skill] = Time.time + cooldownDuration; } public void ResetCooldown(SkillData skill) { cooldownTracker.Remove(skill); } public void ResetAllCooldowns() { cooldownTracker.Clear(); } /// /// 현재 실행 중인 스킬 상태를 정리합니다. /// private void ClearCurrentSkillState() { currentSkill = null; currentLoadoutEntry = null; currentCastStartEffects.Clear(); currentTriggeredEffects.Clear(); currentCastStartAbnormalities.Clear(); currentTriggeredAbnormalities.Clear(); currentTriggeredTargetsBuffer.Clear(); currentTargetOverride = null; waitingForEndAnimation = false; currentRepeatCount = 1; currentIterationIndex = 0; } /// /// 현재 트리거 인덱스에 연결된 젬 이상상태를 적중 대상에게 적용합니다. /// private void ApplyTriggeredAbnormalities(int index, List referenceEffects) { if (!currentTriggeredAbnormalities.TryGetValue(index, out List abnormalities) || abnormalities == null || abnormalities.Count == 0) { return; } currentTriggeredTargetsBuffer.Clear(); for (int i = 0; i < referenceEffects.Count; i++) { SkillEffect effect = referenceEffects[i]; if (effect == null || effect.TargetType == TargetType.Self) continue; effect.CollectTargets(gameObject, currentTriggeredTargetsBuffer, currentTargetOverride); } if (currentTriggeredTargetsBuffer.Count == 0) { if (debugMode) { Debug.LogWarning($"[Skill] Trigger abnormality skipped - no hit target resolved for index {index}"); } return; } for (int i = 0; i < currentTriggeredTargetsBuffer.Count; i++) { GameObject target = currentTriggeredTargetsBuffer[i]; if (target == null) continue; AbnormalityManager abnormalityManager = target.GetComponent(); if (abnormalityManager == null) continue; for (int j = 0; j < abnormalities.Count; j++) { AbnormalityData abnormality = abnormalities[j]; if (abnormality == null) continue; if (debugMode) { Debug.Log($"[Skill] Trigger abnormality: {abnormality.abnormalityName} -> {target.name} (index {index})"); } abnormalityManager.ApplyAbnormality(abnormality, gameObject); } } currentTriggeredTargetsBuffer.Clear(); } } }