using UnityEngine; using System.Collections.Generic; using Unity.Netcode; namespace Colosseum.Skills { /// /// 스킬 실행을 관리하는 컴포넌트. /// 애니메이션 이벤트 기반으로 효과가 발동됩니다. /// 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; // 현재 실행 중인 스킬 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; private void Awake() { 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; if (debugMode) Debug.Log($"[Skill] Cast: {skill.SkillName}"); // 쿨타임 시작 StartCooldown(skill); // 스킬 애니메이션 재생 if (skill.SkillClip != null && animator != null) { animator.speed = skill.AnimationSpeed; PlaySkillClip(skill.SkillClip); } 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; } 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 void CancelSkill() { if (currentSkill != null) { if (debugMode) Debug.Log($"Skill cancelled: {currentSkill.SkillName}"); RestoreBaseController(); currentSkill = null; skillEndRequested = false; waitingForEndAnimation = false; } } 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(); } } }