diff --git a/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset b/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset index bd268336..88434715 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset @@ -19,6 +19,11 @@ MonoBehaviour: endClip: {fileID: 0} useRootMotion: 1 ignoreRootMotionY: 1 + isEvadeSkill: 1 + blockMovementWhileCasting: 1 + blockJumpWhileCasting: 1 + blockOtherSkillsWhileCasting: 1 + blockEvadeWhileCasting: 1 cooldown: 10 manaCost: 0 effects: [] diff --git a/Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab b/Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab index c9596450..6d63be85 100644 --- a/Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab +++ b/Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab @@ -453,6 +453,8 @@ MonoBehaviour: abnormalityManager: {fileID: 0} actionState: {fileID: 0} networkController: {fileID: 0} + skillInput: {fileID: 0} + skillController: {fileID: 0} stunData: {fileID: 0} silenceData: {fileID: 0} runOnStartInEditor: 0 diff --git a/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs b/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs index 6c009761..929fe005 100644 --- a/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs +++ b/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs @@ -91,6 +91,10 @@ namespace Colosseum.Player GUILayout.BeginVertical(); GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}"); + if (TryGetComponent(out var actionState)) + { + GUILayout.Label($"회피 상태: {(actionState.IsEvading ? "Evading" : "Idle")} / 이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills}"); + } GUILayout.Label("입력 예시: 기절 / Data_Abnormality_Player_Stun / 0"); GUI.SetNextControlName("AbnormalityInputField"); diff --git a/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs b/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs index e1c12060..fe1070a0 100644 --- a/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs +++ b/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using UnityEngine; @@ -5,6 +6,7 @@ using UnityEngine; using Unity.Netcode; using Colosseum.Abnormalities; +using Colosseum.Skills; #if UNITY_EDITOR using UnityEditor; @@ -23,6 +25,8 @@ namespace Colosseum.Player [SerializeField] private AbnormalityManager abnormalityManager; [SerializeField] private PlayerActionState actionState; [SerializeField] private PlayerNetworkController networkController; + [SerializeField] private PlayerSkillInput skillInput; + [SerializeField] private SkillController skillController; [Header("Test Data")] [SerializeField] private AbnormalityData stunData; @@ -100,6 +104,9 @@ namespace Colosseum.Player Verify("초기 상태: 사망 아님", !networkController.IsDead); Verify("초기 상태: 이동 가능", actionState.CanMove); Verify("초기 상태: 스킬 사용 가능", actionState.CanUseSkills); + Verify("초기 상태: 회피 상태 아님", !actionState.IsEvading); + + yield return RunEvadeVerification(); abnormalityManager.ApplyAbnormality(stunData, gameObject); yield return new WaitForSeconds(settleDelay); @@ -176,6 +183,10 @@ namespace Colosseum.Player actionState = GetComponent(); if (networkController == null) networkController = GetComponent(); + if (skillInput == null) + skillInput = GetComponent(); + if (skillController == null) + skillController = GetComponent(); } private void LoadDefaultAssetsIfNeeded() @@ -222,5 +233,70 @@ namespace Colosseum.Player return Debug.isDebugBuild; #endif } + + private IEnumerator RunEvadeVerification() + { + SkillData evadeSkill = skillInput != null ? skillInput.GetSkill(6) : null; + if (evadeSkill == null) + { + AppendLine("[SKIP] 회피 검증: 회피 슬롯 스킬이 없습니다."); + yield break; + } + + if (skillController == null || !skillController.ExecuteSkill(evadeSkill)) + { + Verify("회피 검증: 스킬 실행 성공", false); + yield break; + } + + yield return new WaitForSeconds(settleDelay); + + Verify("회피 적용: IsEvading", actionState.IsEvading); + Verify("회피 적용: 이동 불가", !actionState.CanMove); + Verify("회피 적용: 점프 불가", !actionState.CanJump); + Verify("회피 적용: 일반 스킬 사용 불가", !actionState.CanUseSkills); + Verify("회피 적용: 회피 스킬 연속 사용 불가", !actionState.CanStartSkill(evadeSkill)); + + if (silenceData != null) + { + abnormalityManager.ApplyAbnormality(silenceData, gameObject); + yield return WaitForConditionOrTimeout(() => actionState.IsSilenced, settleDelay + 0.5f); + + Verify("회피 중 침묵 적용: IsSilenced", actionState.IsSilenced); + Verify("회피 중 침묵 적용: 회피 상태 유지", actionState.IsEvading); + Verify("회피 중 침묵 적용: 회피 스킬 신규 사용 불가", !actionState.CanStartSkill(evadeSkill)); + + abnormalityManager.RemoveAbnormality(silenceData); + yield return new WaitForSeconds(settleDelay); + } + + yield return WaitForConditionOrTimeout(() => !actionState.IsEvading, GetSkillDuration(evadeSkill) + 1.5f); + + Verify("회피 해제: IsEvading false", !actionState.IsEvading); + Verify("회피 해제: 이동 가능 복구", actionState.CanMove); + Verify("회피 해제: 스킬 사용 가능 복구", actionState.CanUseSkills); + Verify("회피 해제: 회피 스킬 재사용 가능 복구", actionState.CanStartSkill(evadeSkill)); + } + + private float GetSkillDuration(SkillData skill) + { + if (skill == null || skill.SkillClip == null) + return settleDelay; + + return Mathf.Max(settleDelay, skill.SkillClip.length / Mathf.Max(0.1f, skill.AnimationSpeed)); + } + + private IEnumerator WaitForConditionOrTimeout(Func predicate, float timeout) + { + float elapsed = 0f; + while (elapsed < timeout) + { + if (predicate()) + yield break; + + elapsed += Time.deltaTime; + yield return null; + } + } } } diff --git a/Assets/_Game/Scripts/Player/PlayerActionState.cs b/Assets/_Game/Scripts/Player/PlayerActionState.cs index 803cc40f..a147a1db 100644 --- a/Assets/_Game/Scripts/Player/PlayerActionState.cs +++ b/Assets/_Game/Scripts/Player/PlayerActionState.cs @@ -55,6 +55,12 @@ namespace Colosseum.Player /// public SkillData CurrentSkill => skillController != null ? skillController.CurrentSkill : null; + /// + /// 현재 시전 중인 스킬이 회피 스킬일 때의 회피 상태 여부. + /// 침묵은 회피 상태 자체를 끊지 않으며, 회피 스킬의 신규 사용만 막습니다. + /// + public bool IsEvading => IsCastingSkill && CurrentSkill != null && CurrentSkill.IsEvadeSkill; + /// /// 입력을 받아도 되는지 여부 /// @@ -71,14 +77,27 @@ namespace Colosseum.Player public bool CanJump => CanReceiveInput && !IsStunned && !BlocksJumpForCurrentSkill(); /// - /// 스킬 사용 가능 여부 + /// 일반 스킬 시작 가능 여부 /// - public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsSilenced && !BlocksSkillUseForCurrentSkill(); + public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsSilenced && !IsEvading && !BlocksSkillUseForCurrentSkill(); /// - /// 회피 사용 가능 여부 + /// 특정 스킬의 시작 가능 여부. + /// 회피 스킬도 일반 스킬과 같은 시작 판정을 사용하되, 현재 시전 중인 스킬의 회피 차단 정책을 따릅니다. /// - public bool CanEvade => CanReceiveInput && !IsStunned && !IsSilenced && !BlocksEvadeForCurrentSkill(); + public bool CanStartSkill(SkillData skill) + { + if (skill == null) + return false; + + if (!CanReceiveInput || IsStunned || IsSilenced || IsEvading) + return false; + + if (skill.IsEvadeSkill) + return !BlocksEvadeForCurrentSkill(); + + return !BlocksSkillUseForCurrentSkill(); + } /// /// 현재 이동 속도 배율 diff --git a/Assets/_Game/Scripts/Player/PlayerSkillInput.cs b/Assets/_Game/Scripts/Player/PlayerSkillInput.cs index 28a625cb..14caeb33 100644 --- a/Assets/_Game/Scripts/Player/PlayerSkillInput.cs +++ b/Assets/_Game/Scripts/Player/PlayerSkillInput.cs @@ -123,8 +123,6 @@ namespace Colosseum.Player if (slotIndex < 0 || slotIndex >= skillSlots.Length) return; - bool isEvadeSlot = slotIndex == skillSlots.Length - 1; - SkillData skill = skillSlots[slotIndex]; if (skill == null) { @@ -133,14 +131,8 @@ namespace Colosseum.Player } // 사망 상태 체크 - if (actionState != null) - { - if (isEvadeSlot && !actionState.CanEvade) - return; - - if (!isEvadeSlot && !actionState.CanUseSkills) - return; - } + if (actionState != null && !actionState.CanStartSkill(skill)) + return; // 로컬 체크 (빠른 피드백용) if (skillController.IsExecutingSkill) @@ -176,21 +168,12 @@ namespace Colosseum.Player if (slotIndex < 0 || slotIndex >= skillSlots.Length) return; - bool isEvadeSlot = slotIndex == skillSlots.Length - 1; - SkillData skill = skillSlots[slotIndex]; if (skill == null) return; // 서버에서 다시 검증 - // 사망 상태 체크 - if (actionState != null) - { - if (isEvadeSlot && !actionState.CanEvade) - return; - - if (!isEvadeSlot && !actionState.CanUseSkills) - return; - } + if (actionState != null && !actionState.CanStartSkill(skill)) + return; if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill)) return; @@ -279,7 +262,7 @@ namespace Colosseum.Player SkillData skill = GetSkill(slotIndex); if (skill == null) return false; - if (actionState != null && !actionState.CanUseSkills) + if (actionState != null && !actionState.CanStartSkill(skill)) return false; return !skillController.IsOnCooldown(skill) && !skillController.IsExecutingSkill; diff --git a/Assets/_Game/Scripts/Skills/SkillData.cs b/Assets/_Game/Scripts/Skills/SkillData.cs index 7857f12a..5d4531a7 100644 --- a/Assets/_Game/Scripts/Skills/SkillData.cs +++ b/Assets/_Game/Scripts/Skills/SkillData.cs @@ -32,6 +32,8 @@ namespace Colosseum.Skills [SerializeField] private bool jumpToTarget = false; [Header("행동 제한")] + [Tooltip("이 스킬을 회피 상태로 취급할지 여부")] + [SerializeField] private bool isEvadeSkill = false; [Tooltip("시전 중 이동 입력 차단 여부")] [SerializeField] private bool blockMovementWhileCasting = true; [Tooltip("시전 중 점프 입력 차단 여부")] @@ -61,6 +63,7 @@ namespace Colosseum.Skills public bool UseRootMotion => useRootMotion; public bool IgnoreRootMotionY => ignoreRootMotionY; public bool JumpToTarget => jumpToTarget; + public bool IsEvadeSkill => isEvadeSkill; public bool BlockMovementWhileCasting => blockMovementWhileCasting; public bool BlockJumpWhileCasting => blockJumpWhileCasting; public bool BlockOtherSkillsWhileCasting => blockOtherSkillsWhileCasting;