using UnityEngine; using System.Collections.Generic; namespace Colosseum.Skills { /// /// 스킬 실행을 관리하는 컴포넌트. /// 애니메이션 이벤트 기반으로 효과가 발동됩니다. /// public class SkillController : MonoBehaviour { 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("설정")] [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 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; } 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) { 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); } /// /// 종료 클립 재생 /// 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); } /// /// 기본 컨트롤러로 복원 /// private void RestoreBaseController() { 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 (currentSkill == null) { if (debugMode) Debug.LogWarning("[Effect] No skill executing"); 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 (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(); } } }