- FBX 내장 AnimationClip을 개별 .anim 파일로 추출하는 에디터 툴 추가 (AnimationClipExtractor) - 스킬/보스/AnimatorController에서 참조 중인 클립만 선택적 추출 - 추출 후 모든 참조(SkillData, BossPhaseData, AnimatorController)를 .anim으로 자동 relink - 추출 완료된 FBX 자동 삭제 (참조 안전성 검증 포함) - SkillController: registeredClips를 OnValidate에서 _Player_ 이름 기반 자동 등록 - PlayerSkillInput: skillSlots를 OnValidate에서 _Skill_Player_ 이름 기반 자동 등록 - 38개 FBX 삭제, 40+개 .anim 파일로 교체 완료
715 lines
26 KiB
C#
715 lines
26 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
using UnityEngine;
|
|
using Unity.Netcode;
|
|
|
|
using Colosseum.Abnormalities;
|
|
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace Colosseum.Skills
|
|
{
|
|
/// <summary>
|
|
/// 스킬 강제 취소 이유
|
|
/// </summary>
|
|
public enum SkillCancelReason
|
|
{
|
|
None,
|
|
Manual,
|
|
Interrupt,
|
|
Death,
|
|
Stun,
|
|
HitReaction,
|
|
Respawn,
|
|
Revive,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 실행을 관리하는 컴포넌트.
|
|
/// 애니메이션 이벤트 기반으로 효과가 발동됩니다.
|
|
/// </summary>
|
|
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("\"_Player_\" 이름이 포함된 클립이 자동 등록됩니다. 서버→클라이언트 클립 동기화에 사용됩니다.")]
|
|
[SerializeField] private List<AnimationClip> 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<SkillEffect> currentCastStartEffects = new();
|
|
private readonly Dictionary<int, List<SkillEffect>> currentTriggeredEffects = new();
|
|
private readonly List<AbnormalityData> currentCastStartAbnormalities = new();
|
|
private readonly Dictionary<int, List<AbnormalityData>> currentTriggeredAbnormalities = new();
|
|
private readonly List<GameObject> currentTriggeredTargetsBuffer = new();
|
|
private bool waitingForEndAnimation; // EndAnimation 종료 대기 중
|
|
private int currentRepeatCount = 1;
|
|
private int currentIterationIndex = 0;
|
|
private GameObject currentTargetOverride;
|
|
|
|
// 쿨타임 추적
|
|
private Dictionary<SkillData, float> cooldownTracker = new Dictionary<SkillData, float>();
|
|
|
|
|
|
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<Animator>();
|
|
}
|
|
|
|
// 기본 컨트롤러 저장
|
|
if (baseController == null && animator != null)
|
|
{
|
|
baseController = animator.runtimeAnimatorController;
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnValidate()
|
|
{
|
|
AutoRegisterPlayerClips();
|
|
}
|
|
|
|
/// <summary>
|
|
/// "_Player_"가 포함된 모든 AnimationClip을 registeredClips에 자동 등록합니다.
|
|
/// 서버→클라이언트 클립 동기화 인덱스의 일관성을 위해 이름순으로 정렬합니다.
|
|
/// </summary>
|
|
private void AutoRegisterPlayerClips()
|
|
{
|
|
string[] guids = AssetDatabase.FindAssets("t:AnimationClip", new[] { "Assets/_Game/Animations" });
|
|
var clips = new List<AnimationClip>();
|
|
|
|
foreach (string guid in guids)
|
|
{
|
|
string path = AssetDatabase.GUIDToAssetPath(guid);
|
|
string clipName = Path.GetFileNameWithoutExtension(path);
|
|
|
|
if (clipName.IndexOf("_Player_", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
{
|
|
AnimationClip clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(path);
|
|
if (clip != null)
|
|
clips.Add(clip);
|
|
}
|
|
}
|
|
|
|
clips.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.Ordinal));
|
|
|
|
// 변경이 있는 경우만 갱신 (무한 루프 방지)
|
|
bool changed = registeredClips.Count != clips.Count;
|
|
if (!changed)
|
|
{
|
|
for (int i = 0; i < clips.Count; i++)
|
|
{
|
|
if (registeredClips[i] != clips[i])
|
|
{
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changed)
|
|
{
|
|
registeredClips.Clear();
|
|
registeredClips.AddRange(clips);
|
|
Debug.Log($"[SkillController] 자동 등록: {clips.Count}개 Player 클립", this);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 시전
|
|
/// </summary>
|
|
public bool ExecuteSkill(SkillData skill)
|
|
{
|
|
return ExecuteSkill(SkillLoadoutEntry.CreateTemporary(skill));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 슬롯 엔트리 기준으로 스킬 시전
|
|
/// </summary>
|
|
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry)
|
|
{
|
|
currentTargetOverride = null;
|
|
return ExecuteSkillInternal(loadoutEntry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 타겟 오버라이드와 함께 스킬 시전.
|
|
/// SingleAlly 타입 효과에서 외부 타겟을 사용할 때 호출합니다.
|
|
/// </summary>
|
|
public bool ExecuteSkill(SkillLoadoutEntry loadoutEntry, GameObject targetOverride)
|
|
{
|
|
currentTargetOverride = targetOverride;
|
|
return ExecuteSkillInternal(loadoutEntry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 시전 공통 로직
|
|
/// </summary>
|
|
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<Colosseum.Combat.IDamageable>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 시전 시작 즉시 발동하는 효과를 실행합니다.
|
|
/// 서버 권한으로만 처리해 실제 게임플레이 효과가 한 번만 적용되게 합니다.
|
|
/// </summary>
|
|
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<AbnormalityManager>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 애니메이션 이벤트가 없는 자기 자신 대상 효과는 시전 즉시 발동합니다.
|
|
/// 버프/무적 같은 자기 강화 스킬이 이벤트 누락으로 동작하지 않는 상황을 막기 위한 보정입니다.
|
|
/// </summary>
|
|
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<SkillEffect> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 슬롯 엔트리 기준으로 시전 시작/트리거 효과를 합성합니다.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 스킬의 반복 차수 하나를 시작합니다.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 반복 시전이 남아 있으면 다음 차수를 시작합니다.
|
|
/// </summary>
|
|
private bool TryStartNextIteration()
|
|
{
|
|
if (currentSkill == null)
|
|
return false;
|
|
|
|
if (currentIterationIndex >= currentRepeatCount)
|
|
return false;
|
|
|
|
StartCurrentIteration();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 클립으로 Override Controller 생성 후 재생
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 종료 클립 재생
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기본 컨트롤러로 복원
|
|
/// </summary>
|
|
private void RestoreBaseController()
|
|
{
|
|
if (animator != null && baseController != null)
|
|
{
|
|
animator.runtimeAnimatorController = baseController;
|
|
animator.speed = 1f;
|
|
}
|
|
|
|
// 클라이언트에 복원 동기화
|
|
if (IsServer && IsSpawned)
|
|
RestoreBaseControllerClientRpc();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 클라이언트: override 컨트롤러 적용 + 스킬 상태 재생 (원자적 실행으로 타이밍 문제 해결)
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 클라이언트: 기본 컨트롤러 복원
|
|
/// </summary>
|
|
[Rpc(SendTo.NotServer)]
|
|
private void RestoreBaseControllerClientRpc()
|
|
{
|
|
if (animator != null && baseController != null)
|
|
animator.runtimeAnimatorController = baseController;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 애니메이션 이벤트에서 호출. Effect 리스트의 index번째 효과를 발동합니다.
|
|
/// Animation Event: Function = OnEffect, Int Parameter = effect index (0-based)
|
|
/// </summary>
|
|
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<Colosseum.Combat.IDamageable>();
|
|
if (damageable != null && damageable.IsDead)
|
|
{
|
|
if (debugMode) Debug.Log($"[Effect] Cancelled - owner is dead");
|
|
return;
|
|
}
|
|
|
|
if (!currentTriggeredEffects.TryGetValue(index, out List<SkillEffect> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 애니메이션 이벤트에서 호출. 스킬 종료를 요청합니다.
|
|
/// 애니메이션은 끝까지 재생된 후 종료됩니다.
|
|
/// Animation Event: Function = OnSkillEnd
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 스킬을 강제 취소합니다.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 실행 중인 스킬 상태를 정리합니다.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 트리거 인덱스에 연결된 젬 이상상태를 적중 대상에게 적용합니다.
|
|
/// </summary>
|
|
private void ApplyTriggeredAbnormalities(int index, List<SkillEffect> referenceEffects)
|
|
{
|
|
if (!currentTriggeredAbnormalities.TryGetValue(index, out List<AbnormalityData> 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<AbnormalityManager>();
|
|
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();
|
|
}
|
|
}
|
|
}
|