using UnityEngine; using System.Collections.Generic; using Unity.Netcode; namespace Colosseum.Skills { /// /// 스킬 강제 취소 이유 /// public enum SkillCancelReason { None, Manual, Interrupt, Death, Stun, HitReaction, Respawn, } /// /// 스킬 실행을 관리하는 컴포넌트. /// 애니메이션 이벤트 기반으로 효과가 발동됩니다. /// 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 bool skillEndRequested; // OnSkillEnd 이벤트 호출 여부 private bool waitingForEndAnimation; // EndAnimation 종료 대기 중 // 쿨타임 추적 private Dictionary cooldownTracker = new Dictionary(); public bool IsExecutingSkill => currentSkill != null && !skillEndRequested; 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 Animator Animator => animator; public SkillCancelReason LastCancelReason => lastCancelReason; public string LastCancelledSkillName => lastCancelledSkillName; 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(); currentSkill = null; } return; } // 애니메이션 종료 시 처리 (OnSkillEnd 여부와 관계없이 애니메이션 끝까지 재생) if (stateInfo.normalizedTime >= 1f) { 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(); currentSkill = null; } } } /// /// 스킬 시전 /// public bool ExecuteSkill(SkillData skill) { 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; } currentSkill = skill; skillEndRequested = false; waitingForEndAnimation = false; lastCancelReason = SkillCancelReason.None; if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}"); // 쿨타임 시작 StartCooldown(skill); TriggerCastStartEffects(skill); // 스킬 애니메이션 재생 if (skill.SkillClip != null && animator != null) { animator.speed = skill.AnimationSpeed; PlaySkillClip(skill.SkillClip); } TriggerImmediateSelfEffectsIfNeeded(skill); return true; } /// /// 시전 시작 즉시 발동하는 효과를 실행합니다. /// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다. /// private void TriggerCastStartEffects(SkillData skill) { if (skill == null || skill.CastStartEffects == null || skill.CastStartEffects.Count == 0) return; if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return; for (int i = 0; i < skill.CastStartEffects.Count; i++) { SkillEffect effect = skill.CastStartEffects[i]; if (effect == null) continue; if (debugMode) Debug.Log($"[Skill] Cast start effect: {effect.name} (index {i})"); effect.ExecuteOnCast(gameObject); } } /// /// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다. /// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다. /// private void TriggerImmediateSelfEffectsIfNeeded(SkillData skill) { if (skill == null || skill.Effects == null || skill.Effects.Count == 0) return; if (NetworkManager.Singleton != null && !NetworkManager.Singleton.IsServer) return; if (skill.SkillClip != null && skill.SkillClip.events != null && skill.SkillClip.events.Length > 0) return; for (int i = 0; i < skill.Effects.Count; i++) { SkillEffect effect = skill.Effects[i]; if (effect == null || effect.TargetType != TargetType.Self) continue; if (debugMode) Debug.Log($"[Skill] Immediate self effect: {effect.name} (index {i})"); effect.ExecuteOnCast(gameObject); } } /// /// 스킬 클립으로 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; } var effects = currentSkill.Effects; if (index < 0 || index >= effects.Count) { if (debugMode) Debug.LogWarning($"[Effect] Invalid index: {index}"); return; } var effect = effects[index]; if (debugMode) Debug.Log($"[Effect] {effect.name} (index {index})"); // 공격 범위 시각화 if (showAreaDebug) { effect.DrawDebugRange(gameObject, debugDrawDuration); } effect.ExecuteOnCast(gameObject); } /// /// 애니메이션 이벤트에서 호출. 스킬 종료를 요청합니다. /// 애니메이션은 끝까지 재생된 후 종료됩니다. /// 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; } skillEndRequested = true; if (debugMode) Debug.Log($"[Skill] End requested: {currentSkill.SkillName} (will complete after animation)"); } /// /// 현재 스킬을 강제 취소합니다. /// 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(); currentSkill = null; skillEndRequested = false; waitingForEndAnimation = false; 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) { cooldownTracker[skill] = Time.time + skill.Cooldown; } public void ResetCooldown(SkillData skill) { cooldownTracker.Remove(skill); } public void ResetAllCooldowns() { cooldownTracker.Clear(); } } }