From 147e9baa255fd9b2e95992a089a23e4e5b67ab21 Mon Sep 17 00:00:00 2001 From: dal4segno Date: Mon, 6 Apr 2026 18:03:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=8C=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EA=B2=BD=EC=A7=81/=EB=8B=A4=EC=9A=B4=20=ED=9A=8C=EB=B3=B5=20?= =?UTF-8?q?=EA=B5=AC=EA=B0=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HitReactionController에 경직, 다운 회복 가능 구간, Hit 속도 배율 파라미터 처리를 추가 - DownBegin/Recover 상태 종료를 StateMachineBehaviour로 받아 구르기 허용 구간과 다운 해제를 분리 - 드로그 발구르기를 경직 이펙트로 전환하고 넉백/경직 이펙트에서 Hit 애니메이션 속도 배율을 설정 가능하게 정리 - PlayMode 테스트를 추가해 경직/넉백이 Hit 애니메이션 속도 배율을 실제로 반영하는지 자동 검증 --- .../Controllers/AC_Player_Default.controller | 120 ++- .../Skills/Data_Skill_Drog_발구르기.asset | 2 +- ...ta_SkillEffect_Drog_도약_착지_1_넉백.asset | 2 + ...ata_SkillEffect_Drog_발구르기_1_경직.asset | 29 + ...killEffect_Drog_발구르기_1_경직.asset.meta | 8 + .../Data_SkillEffect_Drog_밟기_1_넉백.asset | 2 + .../Scripts/Editor/RebuildDrogCombatAssets.cs | 48 +- .../Scripts/Player/HitReactionController.cs | 246 +++++- .../Player/PlayerAbnormalityDebugHUD.cs | 3 +- .../PlayerAbnormalityVerificationRunner.cs | 34 +- .../_Game/Scripts/Player/PlayerActionState.cs | 42 +- .../Player/PlayerDownBeginExitBehaviour.cs | 19 + .../PlayerDownBeginExitBehaviour.cs.meta | 2 + .../Player/PlayerDownRecoverExitBehaviour.cs | 19 + .../PlayerDownRecoverExitBehaviour.cs.meta | 2 + .../Scripts/Skills/Effects/KnockbackEffect.cs | 9 +- .../Scripts/Skills/Effects/StaggerEffect.cs | 44 + .../Skills/Effects/StaggerEffect.cs.meta | 2 + .../_Game/Scripts/Skills/SkillController.cs | 8 + Assets/_Game/Scripts/Skills/SkillData.cs | 11 + Assets/_Game/Tests/PlayMode.meta | 8 + .../PlayMode/Colosseum.Tests.PlayMode.asmdef | 23 + .../Colosseum.Tests.PlayMode.asmdef.meta | 7 + .../HitReactionAnimationSpeedTests.cs | 191 +++++ .../HitReactionAnimationSpeedTests.cs.meta | 2 + Assets/_Game/Tests/PlayMode/Resources.meta | 8 + .../AC_Player_Default_PlayModeTest.controller | 804 ++++++++++++++++++ ...layer_Default_PlayModeTest.controller.meta | 8 + 28 files changed, 1665 insertions(+), 38 deletions(-) create mode 100644 Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset create mode 100644 Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset.meta create mode 100644 Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs create mode 100644 Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs.meta create mode 100644 Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs create mode 100644 Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs.meta create mode 100644 Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs create mode 100644 Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs.meta create mode 100644 Assets/_Game/Tests/PlayMode.meta create mode 100644 Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef create mode 100644 Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef.meta create mode 100644 Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs create mode 100644 Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs.meta create mode 100644 Assets/_Game/Tests/PlayMode/Resources.meta create mode 100644 Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller create mode 100644 Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller.meta diff --git a/Assets/_Game/Animations/Controllers/AC_Player_Default.controller b/Assets/_Game/Animations/Controllers/AC_Player_Default.controller index 937ece40..2d4f47fe 100644 --- a/Assets/_Game/Animations/Controllers/AC_Player_Default.controller +++ b/Assets/_Game/Animations/Controllers/AC_Player_Default.controller @@ -27,6 +27,33 @@ AnimatorState: m_MirrorParameter: m_CycleOffsetParameter: m_TimeParameter: +--- !u!1102 &-7325123755187262477 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Hit + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: -4328548395604843749} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 1 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: -4467167322993697325, guid: 575336e929435dc4398adefbfa9cde78, type: 3} + m_Tag: + m_SpeedParameter: HitSpeedMultiplier + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: --- !u!1101 &-5485275494767118128 AnimatorStateTransition: m_ObjectHideFlags: 1 @@ -88,12 +115,16 @@ AnimatorStateMachine: - serializedVersion: 1 m_State: {fileID: 1111345123456789014} m_Position: {x: -1260, y: 140, z: 0} + - serializedVersion: 1 + m_State: {fileID: -7325123755187262477} + m_Position: {x: -450, y: 60, z: 0} m_ChildStateMachines: [] m_AnyStateTransitions: - {fileID: -754003289131015157} - {fileID: 469741948129995159} - {fileID: 6228136561094308872} - {fileID: 1111345123456789021} + - {fileID: -4832749926410887626} m_EntryTransitions: [] m_StateMachineTransitions: {} m_StateMachineBehaviours: [] @@ -102,6 +133,31 @@ AnimatorStateMachine: m_ExitPosition: {x: 280, y: 270, z: 0} m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} m_DefaultState: {fileID: -7908033645098541312} +--- !u!1101 &-4832749926410887626 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Hit + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7325123755187262477} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0.75 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 --- !u!1102 &-4652545120162758660 AnimatorState: serializedVersion: 6 @@ -129,6 +185,40 @@ AnimatorState: m_MirrorParameter: m_CycleOffsetParameter: m_TimeParameter: +--- !u!1101 &-4328548395604843749 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7908033645098541312} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0.95 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!114 &-2635712880128095960 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 372818b6c3ad2c3028f7411ec532d127, type: 3} + m_Name: + m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerDownRecoverExitBehaviour --- !u!1102 &-2487449162152911812 AnimatorState: serializedVersion: 6 @@ -345,6 +435,18 @@ AnimatorController: m_DefaultInt: 0 m_DefaultBool: 0 m_Controller: {fileID: 9100000} + - m_Name: Hit + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: HitSpeedMultiplier + m_Type: 1 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} m_AnimatorLayers: - serializedVersion: 5 m_Name: Base Layer @@ -421,7 +523,8 @@ AnimatorState: m_CycleOffset: 0 m_Transitions: - {fileID: 1111345123456789022} - m_StateMachineBehaviours: [] + m_StateMachineBehaviours: + - {fileID: 2761124885249802742} m_Position: {x: 50, y: 50, z: 0} m_IKOnFeet: 0 m_WriteDefaultValues: 1 @@ -475,7 +578,8 @@ AnimatorState: m_CycleOffset: 0 m_Transitions: - {fileID: 1111345123456789024} - m_StateMachineBehaviours: [] + m_StateMachineBehaviours: + - {fileID: -2635712880128095960} m_Position: {x: 50, y: 50, z: 0} m_IKOnFeet: 0 m_WriteDefaultValues: 1 @@ -584,6 +688,18 @@ AnimatorStateTransition: m_InterruptionSource: 0 m_OrderedInterruption: 1 m_CanTransitionToSelf: 1 +--- !u!114 &2761124885249802742 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 05c1f0fd1467993a7992e73162aebccc, type: 3} + m_Name: + m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerDownBeginExitBehaviour --- !u!1101 &3552691702041008167 AnimatorStateTransition: m_ObjectHideFlags: 1 diff --git a/Assets/_Game/Data/Skills/Data_Skill_Drog_발구르기.asset b/Assets/_Game/Data/Skills/Data_Skill_Drog_발구르기.asset index 7d24b806..ffc84625 100644 --- a/Assets/_Game/Data/Skills/Data_Skill_Drog_발구르기.asset +++ b/Assets/_Game/Data/Skills/Data_Skill_Drog_발구르기.asset @@ -37,7 +37,7 @@ MonoBehaviour: maxGemSlotCount: 0 castStartEffects: - {fileID: 11400000, guid: 9aff354899593121f89a55ada9ed27c8, type: 2} - - {fileID: 11400000, guid: 3952a1b7ae34d1ee5b0b4bfc9faad041, type: 2} + - {fileID: 11400000, guid: 86d49ab180b62b395a64b1ba88045d97, type: 2} triggeredEffects: [] isChanneling: 0 channelDuration: 3 diff --git a/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_도약_착지_1_넉백.asset b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_도약_착지_1_넉백.asset index d9ede9fe..479ecb1f 100644 --- a/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_도약_착지_1_넉백.asset +++ b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_도약_착지_1_넉백.asset @@ -27,3 +27,5 @@ MonoBehaviour: force: 8 upwardForce: 2 duration: 0.25 + playHitAnimation: 1 + hitAnimationSpeedMultiplier: 1 diff --git a/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset new file mode 100644 index 00000000..ccef49f6 --- /dev/null +++ b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset @@ -0,0 +1,29 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 675a8e51dce8ee00db904424ae5c8d9d, type: 3} + m_Name: "Data_SkillEffect_Drog_\uBC1C\uAD6C\uB974\uAE30_1_\uACBD\uC9C1" + m_EditorClassIdentifier: Colosseum.Game::Colosseum.Skills.Effects.StaggerEffect + targetType: 1 + targetTeam: 0 + areaCenter: 0 + areaShape: 0 + targetLayers: + serializedVersion: 2 + m_Bits: 0 + includeCasterInArea: 0 + areaRadius: 4.75 + fanOriginDistance: 1 + fanRadius: 4.75 + fanHalfAngle: 180 + duration: 0.35 + playHitAnimation: 1 + hitAnimationSpeedMultiplier: 1 diff --git a/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset.meta b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset.meta new file mode 100644 index 00000000..9ec45b3d --- /dev/null +++ b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_발구르기_1_경직.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 86d49ab180b62b395a64b1ba88045d97 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_밟기_1_넉백.asset b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_밟기_1_넉백.asset index ac9453ca..08570b85 100644 --- a/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_밟기_1_넉백.asset +++ b/Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Drog_밟기_1_넉백.asset @@ -27,3 +27,5 @@ MonoBehaviour: force: 5 upwardForce: 1 duration: 0.18 + playHitAnimation: 1 + hitAnimationSpeedMultiplier: 1 diff --git a/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs b/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs index c0c81896..8de0655f 100644 --- a/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs +++ b/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs @@ -248,11 +248,11 @@ namespace Colosseum.Editor 180f, AreaCenterType.Caster); - KnockbackEffect stompKnockback = CreateKnockbackEffect( - $"{EffectsFolder}/Data_SkillEffect_Drog_발구르기_1_넉백.asset", - 6f, - 1.5f, - 0.2f, + StaggerEffect stompStagger = CreateStaggerEffect( + $"{EffectsFolder}/Data_SkillEffect_Drog_발구르기_1_경직.asset", + 0.35f, + true, + 1f, AreaShapeType.Sphere, 4.75f, 1f, @@ -277,6 +277,8 @@ namespace Colosseum.Editor 8f, 2f, 0.25f, + true, + 1f, AreaShapeType.Sphere, 4.2f, 1f, @@ -302,6 +304,8 @@ namespace Colosseum.Editor 5f, 1f, 0.18f, + true, + 1f, AreaShapeType.Sphere, 2.8f, 1f, @@ -513,7 +517,7 @@ namespace Colosseum.Editor true, false, stompDamage, - stompKnockback); + stompStagger); SkillData leapPrepareSkill = CreateSkill( $"{SkillsFolder}/Data_Skill_Drog_도약_준비.asset", @@ -1050,6 +1054,8 @@ namespace Colosseum.Editor float force, float upwardForce, float duration, + bool playHitAnimation, + float hitAnimationSpeedMultiplier, AreaShapeType areaShape, float areaRadius, float fanOriginDistance, @@ -1064,6 +1070,36 @@ namespace Colosseum.Editor serializedObject.FindProperty("force").floatValue = force; serializedObject.FindProperty("upwardForce").floatValue = upwardForce; serializedObject.FindProperty("duration").floatValue = duration; + serializedObject.FindProperty("playHitAnimation").boolValue = playHitAnimation; + serializedObject.FindProperty("hitAnimationSpeedMultiplier").floatValue = hitAnimationSpeedMultiplier; + serializedObject.ApplyModifiedPropertiesWithoutUndo(); + + EditorUtility.SetDirty(effect); + return effect; + } + + /// + /// 범위 경직 효과를 생성하거나 갱신합니다. + /// + private static StaggerEffect CreateStaggerEffect( + string path, + float duration, + bool playHitAnimation, + float hitAnimationSpeedMultiplier, + AreaShapeType areaShape, + float areaRadius, + float fanOriginDistance, + float fanRadius, + float fanHalfAngle, + AreaCenterType areaCenter) + { + StaggerEffect effect = LoadOrCreateAsset(path); + SerializedObject serializedObject = new SerializedObject(effect); + + ConfigureAreaEffect(serializedObject, areaShape, areaRadius, fanOriginDistance, fanRadius, fanHalfAngle, areaCenter); + serializedObject.FindProperty("duration").floatValue = duration; + serializedObject.FindProperty("playHitAnimation").boolValue = playHitAnimation; + serializedObject.FindProperty("hitAnimationSpeedMultiplier").floatValue = hitAnimationSpeedMultiplier; serializedObject.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(effect); diff --git a/Assets/_Game/Scripts/Player/HitReactionController.cs b/Assets/_Game/Scripts/Player/HitReactionController.cs index e493f038..2b7794c9 100644 --- a/Assets/_Game/Scripts/Player/HitReactionController.cs +++ b/Assets/_Game/Scripts/Player/HitReactionController.cs @@ -9,7 +9,7 @@ namespace Colosseum.Player { /// /// 플레이어의 피격 제어 상태를 관리합니다. - /// 넉백 강제 이동과 다운 상태, 피격 애니메이션 재생을 담당합니다. + /// 경직, 넉백, 다운 상태와 피격 애니메이션 재생을 담당합니다. /// [DisallowMultipleComponent] [RequireComponent(typeof(PlayerMovement))] @@ -35,6 +35,9 @@ namespace Colosseum.Player [Tooltip("일반 피격 트리거 이름")] [SerializeField] private string hitTriggerParam = "Hit"; + [Tooltip("일반 피격 애니메이션 속도 배율 파라미터 이름")] + [SerializeField] private string hitSpeedMultiplierParam = "HitSpeedMultiplier"; + [Tooltip("다운 시작 트리거 이름")] [SerializeField] private string downTriggerParam = "Down"; @@ -42,21 +45,42 @@ namespace Colosseum.Player [SerializeField] private string recoverTriggerParam = "Recover"; [Header("Settings")] + [Tooltip("DownBegin 종료 후 구르기 가능 구간까지 대기 시간")] + [Min(0f)] [SerializeField] private float downRecoverableDelayAfterBeginExit = 0.2f; + [Tooltip("애니메이션 파라미터가 없을 때 경고 로그 출력")] [SerializeField] private bool logMissingAnimationParams = false; private readonly NetworkVariable isDowned = new NetworkVariable(false); + private readonly NetworkVariable isDownRecoverable = new NetworkVariable(false); + private readonly NetworkVariable isKnockbackActive = new NetworkVariable(false); + private readonly NetworkVariable isStaggered = new NetworkVariable(false); + private float downRemainingTime; + private float downRecoverableDelayRemaining = -1f; + private float knockbackRemainingTime; + private float staggerRemainingTime; + private bool isDownRecoveryAnimating; /// /// 다운 상태 여부 /// public bool IsDowned => isDowned.Value; + /// + /// 다운 중 구르기 가능 구간 여부 + /// + public bool IsDownRecoverable => isDownRecoverable.Value; + /// /// 넉백 강제 이동 진행 여부 /// - public bool IsKnockbackActive => playerMovement != null && playerMovement.IsForcedMoving; + public bool IsKnockbackActive => isKnockbackActive.Value; + + /// + /// 경직 상태 여부 + /// + public bool IsStaggered => isStaggered.Value; /// /// 피격 반응 무시 상태 여부 @@ -75,20 +99,18 @@ namespace Colosseum.Player private void Update() { - if (!IsServer || !isDowned.Value) + if (!IsServer) return; - downRemainingTime -= Time.deltaTime; - if (downRemainingTime <= 0f) - { - RecoverFromDown(); - } + UpdateKnockbackState(Time.deltaTime); + UpdateStaggerState(Time.deltaTime); + UpdateDownState(Time.deltaTime); } /// - /// 넉백을 적용합니다. + /// 경직을 적용합니다. /// - public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true) + public void ApplyStagger(float duration, bool playHitAnimation = true, float hitAnimationSpeedMultiplier = 1f) { if (!IsServer) return; @@ -98,14 +120,56 @@ namespace Colosseum.Player if (networkController != null && networkController.IsDead) return; - if (IsHitReactionImmune) + if (IsHitReactionImmune || isDowned.Value) return; + if (duration <= 0f) + { + ClearStaggerState(); + return; + } + + staggerRemainingTime = Mathf.Max(staggerRemainingTime, duration); + isStaggered.Value = true; + skillController?.CancelSkill(SkillCancelReason.Stagger); + + if (playHitAnimation) + { + TriggerAnimationRpc(hitTriggerParam, hitAnimationSpeedMultiplier); + } + } + + /// + /// 넉백을 적용합니다. + /// + public void ApplyKnockback(Vector3 worldVelocity, float duration, bool playHitAnimation = true, float hitAnimationSpeedMultiplier = 1f) + { + if (!IsServer) + return; + + ResolveReferences(); + + if (networkController != null && networkController.IsDead) + return; + + if (IsHitReactionImmune || isDowned.Value) + return; + + if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f) + { + ClearKnockbackState(); + playerMovement?.ClearForcedMovement(); + return; + } + + knockbackRemainingTime = Mathf.Max(knockbackRemainingTime, duration); + isKnockbackActive.Value = true; + skillController?.CancelSkill(SkillCancelReason.HitReaction); playerMovement?.ApplyForcedMovement(worldVelocity, duration); if (playHitAnimation) { - TriggerAnimationRpc(hitTriggerParam); + TriggerAnimationRpc(hitTriggerParam, hitAnimationSpeedMultiplier); } } @@ -131,22 +195,48 @@ namespace Colosseum.Player return; isDowned.Value = true; + isDownRecoverable.Value = false; + isDownRecoveryAnimating = false; + downRecoverableDelayRemaining = -1f; + ClearKnockbackState(); + ClearStaggerState(); skillController?.CancelSkill(SkillCancelReason.HitReaction); playerMovement?.ClearForcedMovement(); TriggerAnimationRpc(downTriggerParam); } /// - /// 다운 상태를 해제합니다. + /// DownBegin 종료 시점을 전달받아 구르기 가능 타이머를 시작합니다. /// - public void RecoverFromDown() + public void NotifyDownBeginExited() { - if (!IsServer || !isDowned.Value) + if (!IsServer || !isDowned.Value || isDownRecoveryAnimating) return; - isDowned.Value = false; - downRemainingTime = 0f; - TriggerAnimationRpc(recoverTriggerParam); + downRecoverableDelayRemaining = downRecoverableDelayAfterBeginExit; + } + + /// + /// Recover 상태가 끝났을 때 다운 상태를 최종 해제합니다. + /// + public void NotifyDownRecoverAnimationExited() + { + if (!IsServer || !isDowned.Value || !isDownRecoveryAnimating) + return; + + ClearDownState(); + } + + /// + /// 다운 회복 가능 구간에서 구르기를 사용하며 다운 상태를 종료합니다. + /// + public bool TryConsumeDownRecoverableEvade() + { + if (!IsServer || !isDowned.Value || !isDownRecoverable.Value) + return false; + + ClearDownState(); + return true; } /// @@ -160,12 +250,13 @@ namespace Colosseum.Player ResolveReferences(); playerMovement?.ClearForcedMovement(); + ClearKnockbackState(); + ClearStaggerState(); if (!isDowned.Value) return; - isDowned.Value = false; - downRemainingTime = 0f; + ClearDownState(); if (playRecoverAnimation) { @@ -174,13 +265,18 @@ namespace Colosseum.Player } [Rpc(SendTo.Everyone)] - private void TriggerAnimationRpc(string triggerName) + private void TriggerAnimationRpc(string triggerName, float hitAnimationSpeedMultiplier = 1f) { ResolveReferences(); if (animator == null || string.IsNullOrWhiteSpace(triggerName)) return; + if (triggerName == hitTriggerParam) + { + SetFloatParameterIfExists(hitSpeedMultiplierParam, Mathf.Max(0.01f, hitAnimationSpeedMultiplier)); + } + if (!HasTrigger(triggerName)) { if (logMissingAnimationParams) @@ -193,6 +289,27 @@ namespace Colosseum.Player animator.SetTrigger(triggerName); } + private void SetFloatParameterIfExists(string parameterName, float value) + { + if (animator == null || string.IsNullOrWhiteSpace(parameterName)) + return; + + for (int i = 0; i < animator.parameterCount; i++) + { + AnimatorControllerParameter parameter = animator.GetParameter(i); + if (parameter.type == AnimatorControllerParameterType.Float && parameter.name == parameterName) + { + animator.SetFloat(parameterName, value); + return; + } + } + + if (logMissingAnimationParams) + { + Debug.LogWarning($"[HitReaction] Animator float parameter not found: {parameterName}"); + } + } + private bool HasTrigger(string triggerName) { if (animator == null || string.IsNullOrWhiteSpace(triggerName)) @@ -225,5 +342,92 @@ namespace Colosseum.Player if (animator == null) animator = GetComponentInChildren(); } + + private void UpdateKnockbackState(float deltaTime) + { + if (!isKnockbackActive.Value) + return; + + knockbackRemainingTime -= deltaTime; + if (knockbackRemainingTime <= 0f) + { + ClearKnockbackState(); + } + } + + private void UpdateStaggerState(float deltaTime) + { + if (!isStaggered.Value) + return; + + staggerRemainingTime -= deltaTime; + if (staggerRemainingTime <= 0f) + { + ClearStaggerState(); + } + } + + private void UpdateDownState(float deltaTime) + { + if (!isDowned.Value) + return; + + downRemainingTime -= deltaTime; + + if (!isDownRecoverable.Value && downRecoverableDelayRemaining >= 0f) + { + downRecoverableDelayRemaining -= deltaTime; + if (downRecoverableDelayRemaining <= 0f) + { + EnterDownRecoverableState(); + } + } + + if (downRemainingTime <= 0f) + { + BeginDownRecoveryAnimation(); + } + } + + private void EnterDownRecoverableState() + { + if (!isDowned.Value) + return; + + isDownRecoverable.Value = true; + downRecoverableDelayRemaining = -1f; + } + + private void BeginDownRecoveryAnimation() + { + if (!isDowned.Value || isDownRecoveryAnimating) + return; + + EnterDownRecoverableState(); + isDownRecoveryAnimating = true; + downRemainingTime = 0f; + TriggerAnimationRpc(recoverTriggerParam); + } + + private void ClearDownState() + { + isDowned.Value = false; + isDownRecoverable.Value = false; + isDownRecoveryAnimating = false; + downRemainingTime = 0f; + downRecoverableDelayRemaining = -1f; + } + + private void ClearKnockbackState() + { + isKnockbackActive.Value = false; + knockbackRemainingTime = 0f; + } + + private void ClearStaggerState() + { + isStaggered.Value = false; + staggerRemainingTime = 0f; + } } } diff --git a/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs b/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs index 698a1754..7f189c8c 100644 --- a/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs +++ b/Assets/_Game/Scripts/Player/PlayerAbnormalityDebugHUD.cs @@ -93,7 +93,8 @@ namespace Colosseum.Player GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}"); if (TryGetComponent(out var actionState)) { - GUILayout.Label($"무적:{(actionState.IsDamageImmune ? "On" : "Off")} / 다운:{(actionState.IsDowned ? "On" : "Off")} / 이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills}"); + GUILayout.Label($"무적:{(actionState.IsDamageImmune ? "On" : "Off")} / 경직:{(actionState.IsStaggered ? "On" : "Off")} / 넉백:{(actionState.IsKnockbackActive ? "On" : "Off")} / 다운:{(actionState.IsDowned ? "On" : "Off")} / 다운회복:{(actionState.IsDownRecoverable ? "On" : "Off")}"); + GUILayout.Label($"이동:{actionState.CanMove} / 스킬:{actionState.CanUseSkills} / 구르기:{actionState.CanEvade}"); } GUILayout.Label("입력 예시: 기절 / Data_Abnormality_Player_Stun / 0"); diff --git a/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs b/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs index 4034cb1b..89a07a58 100644 --- a/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs +++ b/Assets/_Game/Scripts/Player/PlayerAbnormalityVerificationRunner.cs @@ -106,6 +106,7 @@ namespace Colosseum.Player Verify("초기 상태: 사망 아님", !networkController.IsDead); Verify("초기 상태: 이동 가능", actionState.CanMove); Verify("초기 상태: 스킬 사용 가능", actionState.CanUseSkills); + Verify("초기 상태: 구르기 가능", actionState.CanEvade); Verify("초기 상태: 무적 상태 아님", !actionState.IsDamageImmune); Verify("초기 상태: 마지막 취소 이유 없음", skillController == null || skillController.LastCancelReason == SkillCancelReason.None); @@ -331,6 +332,12 @@ namespace Colosseum.Player Vector3 knockbackVelocity = Vector3.back * 6f; RequestKnockbackRpc(knockbackVelocity, 0.2f); + yield return new WaitForSeconds(0.05f); + + Verify("넉백 적용: IsKnockbackActive", actionState.IsKnockbackActive); + Verify("넉백 적용: 스킬 사용 불가", !actionState.CanUseSkills); + Verify("넉백 적용: 구르기 불가", !actionState.CanEvade); + yield return new WaitForSeconds(0.3f); float movedDistance = Vector3.Distance(startPosition, transform.position); @@ -371,13 +378,32 @@ namespace Colosseum.Player Verify("다운 적용: 이동 불가", !actionState.CanMove); Verify("다운 적용: 점프 불가", !actionState.CanJump); Verify("다운 적용: 스킬 사용 불가", !actionState.CanUseSkills); + Verify("다운 적용: 초기 구르기 불가", !actionState.CanEvade); Verify("다운 적용: 이동속도 0", Mathf.Approximately(actionState.MoveSpeedMultiplier, 0f)); - yield return WaitForConditionOrTimeout(() => !hitReactionController.IsDowned, 1.5f); + RequestDownBeginExitedRpc(); + yield return WaitForConditionOrTimeout(() => hitReactionController.IsDownRecoverable, settleDelay + 1f); + + Verify("다운 회복 가능 진입: IsDownRecoverable", hitReactionController.IsDownRecoverable); + Verify("다운 회복 가능 진입: 일반 스킬 사용 불가 유지", !actionState.CanUseSkills); + + SkillData evadeSkill = skillInput != null ? skillInput.GetSkill(6) : null; + if (evadeSkill != null) + { + Verify("다운 회복 가능 진입: 구르기 시작 가능", actionState.CanStartSkill(evadeSkill)); + } + else + { + AppendLine("[SKIP] 다운 회복 가능 진입: 구르기 스킬이 없습니다."); + } + + yield return WaitForConditionOrTimeout(() => !hitReactionController.IsDowned, 2.5f); Verify("다운 해제: IsDowned false", !hitReactionController.IsDowned); + Verify("다운 해제: IsDownRecoverable false", !hitReactionController.IsDownRecoverable); Verify("다운 해제: 이동 가능 복구", actionState.CanMove); Verify("다운 해제: 스킬 사용 가능 복구", actionState.CanUseSkills); + Verify("다운 해제: 구르기 가능 복구", actionState.CanEvade); } [Rpc(SendTo.Server)] @@ -386,6 +412,12 @@ namespace Colosseum.Player hitReactionController?.ApplyDown(duration); } + [Rpc(SendTo.Server)] + private void RequestDownBeginExitedRpc() + { + hitReactionController?.NotifyDownBeginExited(); + } + [Rpc(SendTo.Server)] private void RequestKnockbackRpc(Vector3 velocity, float duration) { diff --git a/Assets/_Game/Scripts/Player/PlayerActionState.cs b/Assets/_Game/Scripts/Player/PlayerActionState.cs index a08f75af..8766628f 100644 --- a/Assets/_Game/Scripts/Player/PlayerActionState.cs +++ b/Assets/_Game/Scripts/Player/PlayerActionState.cs @@ -38,11 +38,26 @@ namespace Colosseum.Player /// public bool IsStunned => abnormalityManager != null && abnormalityManager.IsStunned; + /// + /// 경직 상태 여부 + /// + public bool IsStaggered => hitReactionController != null && hitReactionController.IsStaggered; + + /// + /// 넉백 상태 여부 + /// + public bool IsKnockbackActive => hitReactionController != null && hitReactionController.IsKnockbackActive; + /// /// 다운 상태 여부 /// public bool IsDowned => hitReactionController != null && hitReactionController.IsDowned; + /// + /// 다운 중 구르기 가능 구간 여부 + /// + public bool IsDownRecoverable => hitReactionController != null && hitReactionController.IsDownRecoverable; + /// /// 침묵 상태 여부 /// @@ -76,17 +91,28 @@ namespace Colosseum.Player /// /// 플레이어가 직접 이동 입력을 사용할 수 있는지 여부 /// - public bool CanMove => CanReceiveInput && !IsStunned && !IsDowned && !BlocksMovementForCurrentSkill(); + public bool CanMove => CanReceiveInput && !IsStunned && !IsStaggered && !IsKnockbackActive && !IsDowned && !BlocksMovementForCurrentSkill(); /// /// 점프 가능 여부 /// - public bool CanJump => CanReceiveInput && !IsStunned && !IsDowned && !BlocksJumpForCurrentSkill(); + public bool CanJump => CanReceiveInput && !IsStunned && !IsStaggered && !IsKnockbackActive && !IsDowned && !BlocksJumpForCurrentSkill(); /// /// 일반 스킬 시작 가능 여부 /// - public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsDowned && !IsSilenced && !BlocksSkillUseForCurrentSkill(); + public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsStaggered && !IsKnockbackActive && !IsDowned && !IsSilenced && !BlocksSkillUseForCurrentSkill(); + + /// + /// 회피 스킬 시작 가능 여부 + /// + public bool CanEvade => CanReceiveInput + && !IsStunned + && !IsStaggered + && !IsKnockbackActive + && !IsSilenced + && (!IsDowned || IsDownRecoverable) + && !BlocksSkillUseForCurrentSkill(); /// /// 특정 스킬의 시작 가능 여부. @@ -97,9 +123,15 @@ namespace Colosseum.Player if (skill == null) return false; - if (!CanReceiveInput || IsStunned || IsDowned || IsSilenced) + if (!CanReceiveInput || IsStunned || IsStaggered || IsKnockbackActive || IsSilenced) return false; + if (IsDowned) + { + if (!IsDownRecoverable || !skill.IsEvadeSkill) + return false; + } + return !BlocksSkillUseForCurrentSkill(); } @@ -110,7 +142,7 @@ namespace Colosseum.Player { get { - if (!CanReceiveInput || IsStunned || IsDowned) + if (!CanReceiveInput || IsStunned || IsStaggered || IsKnockbackActive || IsDowned) return 0f; return abnormalityManager != null ? abnormalityManager.MoveSpeedMultiplier : 1f; diff --git a/Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs b/Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs new file mode 100644 index 00000000..89f20e3c --- /dev/null +++ b/Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +namespace Colosseum.Player +{ + /// + /// DownBegin 상태 종료를 HitReactionController에 전달합니다. + /// + public class PlayerDownBeginExitBehaviour : StateMachineBehaviour + { + public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) + { + if (animator == null) + return; + + HitReactionController hitReactionController = animator.GetComponentInParent(); + hitReactionController?.NotifyDownBeginExited(); + } + } +} diff --git a/Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs.meta b/Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs.meta new file mode 100644 index 00000000..a0d2cf5d --- /dev/null +++ b/Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 05c1f0fd1467993a7992e73162aebccc \ No newline at end of file diff --git a/Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs b/Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs new file mode 100644 index 00000000..ceced387 --- /dev/null +++ b/Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs @@ -0,0 +1,19 @@ +using UnityEngine; + +namespace Colosseum.Player +{ + /// + /// Recover 상태 종료를 HitReactionController에 전달합니다. + /// + public class PlayerDownRecoverExitBehaviour : StateMachineBehaviour + { + public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) + { + if (animator == null) + return; + + HitReactionController hitReactionController = animator.GetComponentInParent(); + hitReactionController?.NotifyDownRecoverAnimationExited(); + } + } +} diff --git a/Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs.meta b/Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs.meta new file mode 100644 index 00000000..943d07b8 --- /dev/null +++ b/Assets/_Game/Scripts/Player/PlayerDownRecoverExitBehaviour.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 372818b6c3ad2c3028f7411ec532d127 \ No newline at end of file diff --git a/Assets/_Game/Scripts/Skills/Effects/KnockbackEffect.cs b/Assets/_Game/Scripts/Skills/Effects/KnockbackEffect.cs index 56f73c48..c23c33ae 100644 --- a/Assets/_Game/Scripts/Skills/Effects/KnockbackEffect.cs +++ b/Assets/_Game/Scripts/Skills/Effects/KnockbackEffect.cs @@ -15,6 +15,13 @@ namespace Colosseum.Skills.Effects [SerializeField] private float upwardForce = 2f; [Min(0.05f)] [SerializeField] private float duration = 0.2f; + [Header("Hit Animation")] + [Tooltip("넉백 적용 시 경직 애니메이션 재생 여부")] + [SerializeField] private bool playHitAnimation = true; + + [Tooltip("경직 애니메이션 재생 속도 배율")] + [Min(0.01f)] [SerializeField] private float hitAnimationSpeedMultiplier = 1f; + protected override void ApplyEffect(GameObject caster, GameObject target) { if (target == null || caster == null) return; @@ -34,7 +41,7 @@ namespace Colosseum.Skills.Effects if (hitReactionController != null) { - hitReactionController.ApplyKnockback(knockbackVelocity, duration); + hitReactionController.ApplyKnockback(knockbackVelocity, duration, playHitAnimation, hitAnimationSpeedMultiplier); return; } diff --git a/Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs b/Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs new file mode 100644 index 00000000..87a2d1f1 --- /dev/null +++ b/Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs @@ -0,0 +1,44 @@ +using UnityEngine; + +using Colosseum.Player; + +namespace Colosseum.Skills.Effects +{ + /// + /// 대상에게 제자리 경직을 적용하는 스킬 효과입니다. + /// + [CreateAssetMenu(fileName = "StaggerEffect", menuName = "Colosseum/Skills/Effects/Stagger")] + public class StaggerEffect : SkillEffect + { + [Header("Settings")] + [Tooltip("경직 지속 시간")] + [Min(0f)] [SerializeField] private float duration = 0.35f; + + [Tooltip("경직 적용 시 피격 애니메이션 재생 여부")] + [SerializeField] private bool playHitAnimation = true; + + [Tooltip("경직 애니메이션 재생 속도 배율")] + [Min(0.01f)] [SerializeField] private float hitAnimationSpeedMultiplier = 1f; + + protected override void ApplyEffect(GameObject caster, GameObject target) + { + if (target == null) + { + Debug.LogWarning("[StaggerEffect] Target is null."); + return; + } + + HitReactionController hitReactionController = target.GetComponent(); + if (hitReactionController == null) + hitReactionController = target.GetComponentInParent(); + + if (hitReactionController == null) + { + Debug.LogWarning($"[StaggerEffect] HitReactionController not found on target: {target.name}"); + return; + } + + hitReactionController.ApplyStagger(duration, playHitAnimation, hitAnimationSpeedMultiplier); + } + } +} diff --git a/Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs.meta b/Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs.meta new file mode 100644 index 00000000..b384307a --- /dev/null +++ b/Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 675a8e51dce8ee00db904424ae5c8d9d \ No newline at end of file diff --git a/Assets/_Game/Scripts/Skills/SkillController.cs b/Assets/_Game/Scripts/Skills/SkillController.cs index db20aab7..468f8b01 100644 --- a/Assets/_Game/Scripts/Skills/SkillController.cs +++ b/Assets/_Game/Scripts/Skills/SkillController.cs @@ -6,6 +6,7 @@ using UnityEngine; using Unity.Netcode; using Colosseum.Abnormalities; +using Colosseum.Player; #if UNITY_EDITOR using UnityEditor; @@ -23,6 +24,7 @@ namespace Colosseum.Skills Interrupt, Death, Stun, + Stagger, HitReaction, Respawn, Revive, @@ -361,6 +363,12 @@ namespace Colosseum.Skills return false; } + if (skill.IsEvadeSkill) + { + HitReactionController hitReactionController = GetComponent(); + hitReactionController?.TryConsumeDownRecoverableEvade(); + } + currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill); currentSkill = skill; lastCancelReason = SkillCancelReason.None; diff --git a/Assets/_Game/Scripts/Skills/SkillData.cs b/Assets/_Game/Scripts/Skills/SkillData.cs index 79d21d82..3a815e25 100644 --- a/Assets/_Game/Scripts/Skills/SkillData.cs +++ b/Assets/_Game/Scripts/Skills/SkillData.cs @@ -245,6 +245,8 @@ namespace Colosseum.Skills public SkillRoleType SkillRole => skillRole; public SkillActivationType ActivationType => activationType; public SkillBaseType BaseTypes => baseTypes; + public bool IsEvadeSkill => ((baseTypes & SkillBaseType.Mobility) != 0) + && (ContainsEvadeKeyword(skillName) || ContainsEvadeKeyword(name)); /// /// 순차 재생할 클립 목록입니다. /// @@ -307,6 +309,15 @@ namespace Colosseum.Skills return (equippedTraits & allowedWeaponTraits) == allowedWeaponTraits; } + + private static bool ContainsEvadeKeyword(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return false; + + return value.IndexOf("구르기", StringComparison.OrdinalIgnoreCase) >= 0 + || value.IndexOf("회피", StringComparison.OrdinalIgnoreCase) >= 0; + } } /// diff --git a/Assets/_Game/Tests/PlayMode.meta b/Assets/_Game/Tests/PlayMode.meta new file mode 100644 index 00000000..dd2df83f --- /dev/null +++ b/Assets/_Game/Tests/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f666991c90023e380b39b439b8407864 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef b/Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef new file mode 100644 index 00000000..5e3755d9 --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef @@ -0,0 +1,23 @@ +{ + "name": "Colosseum.Tests.PlayMode", + "rootNamespace": "Colosseum.Tests", + "references": [ + "Colosseum.Game", + "Unity.Netcode.Runtime", + "Unity.Networking.Transport" + ], + "optionalUnityReferences": [ + "TestAssemblies" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef.meta b/Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef.meta new file mode 100644 index 00000000..7a3651f7 --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/Colosseum.Tests.PlayMode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a28b72c4fca3e56d7a3e149189f1c976 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs b/Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs new file mode 100644 index 00000000..7f1cb829 --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs @@ -0,0 +1,191 @@ +using System.Collections; + +using NUnit.Framework; + +using UnityEngine; +using UnityEngine.TestTools; + +using Unity.Netcode; +using Unity.Netcode.Transports.UTP; + +using Colosseum.Player; + +namespace Colosseum.Tests +{ + /// + /// 피격 반응 애니메이션의 속도 배율 반영을 검증하는 PlayMode 테스트입니다. + /// + public class HitReactionAnimationSpeedTests + { + private const string AnimatorControllerResourcePath = "AC_Player_Default_PlayModeTest"; + private const string HitStateName = "Hit"; + private const string HitSpeedMultiplierParam = "HitSpeedMultiplier"; + private const float AnimationSampleWindow = 0.08f; + + private GameObject networkManagerObject; + private NetworkManager networkManager; + private GameObject playerObject; + private HitReactionController hitReactionController; + private Animator animator; + + [UnitySetUp] + public IEnumerator SetUp() + { + CleanupTestObjects(); + + networkManagerObject = new GameObject("TestNetworkManager"); + networkManager = networkManagerObject.AddComponent(); + + UnityTransport transport = networkManagerObject.AddComponent(); + transport.SetConnectionData("127.0.0.1", AllocateTestPort()); + + networkManager.NetworkConfig = new NetworkConfig + { + NetworkTransport = transport, + }; + + Assert.IsTrue(networkManager.StartHost(), "테스트용 호스트 시작에 실패했습니다."); + yield return null; + + RuntimeAnimatorController controller = Resources.Load(AnimatorControllerResourcePath); + Assert.NotNull(controller, $"테스트용 AnimatorController를 찾을 수 없습니다: Resources/{AnimatorControllerResourcePath}"); + + playerObject = new GameObject("HitReactionTestPlayer"); + playerObject.SetActive(false); + playerObject.transform.position = Vector3.zero; + + playerObject.AddComponent(); + hitReactionController = playerObject.AddComponent(); + + GameObject visualObject = new GameObject("Visual"); + visualObject.transform.SetParent(playerObject.transform, false); + + animator = visualObject.AddComponent(); + animator.runtimeAnimatorController = controller; + animator.applyRootMotion = false; + + playerObject.SetActive(true); + yield return null; + + NetworkObject networkObject = playerObject.GetComponent(); + networkObject.SpawnWithOwnership(networkManager.LocalClientId); + yield return null; + + Assert.NotNull(hitReactionController, "HitReactionController를 생성하지 못했습니다."); + Assert.NotNull(animator, "테스트용 Animator를 생성하지 못했습니다."); + } + + [UnityTearDown] + public IEnumerator TearDown() + { + if (networkManager != null && networkManager.IsListening) + { + networkManager.Shutdown(); + } + + yield return null; + + CleanupTestObjects(); + yield return null; + } + + [UnityTest] + public IEnumerator ApplyStagger_ReusesHitAnimationAndReflectsSpeedMultiplier() + { + float slowDelta = 0f; + yield return MeasureHitPlaybackDelta( + () => hitReactionController.ApplyStagger(0.4f, true, 0.5f), + 0.5f, + value => slowDelta = value); + + float fastDelta = 0f; + yield return MeasureHitPlaybackDelta( + () => hitReactionController.ApplyStagger(0.4f, true, 2f), + 2f, + value => fastDelta = value); + + Assert.Greater(slowDelta, 0.01f, "느린 경직 재생에서 normalizedTime이 전진하지 않았습니다."); + Assert.Greater(fastDelta, slowDelta * 1.75f, $"빠른 경직 재생 전진량이 충분하지 않습니다. slow={slowDelta:F3}, fast={fastDelta:F3}"); + } + + [UnityTest] + public IEnumerator ApplyKnockback_AlsoAppliesHitAnimationSpeedMultiplier() + { + float measuredDelta = 0f; + yield return MeasureHitPlaybackDelta( + () => hitReactionController.ApplyKnockback(Vector3.back * 3f, 0.25f, true, 1.6f), + 1.6f, + value => measuredDelta = value); + + Assert.Greater(measuredDelta, 0.01f, "넉백에서 Hit 애니메이션이 재생되지 않았습니다."); + } + + private IEnumerator MeasureHitPlaybackDelta(System.Action applyReaction, float expectedSpeedMultiplier, System.Action setMeasuredDelta) + { + hitReactionController.ClearHitReactionState(); + animator.Rebind(); + animator.Update(0f); + yield return null; + + applyReaction.Invoke(); + yield return WaitForHitState(); + + float actualMultiplier = animator.GetFloat(HitSpeedMultiplierParam); + Assert.AreEqual(expectedSpeedMultiplier, actualMultiplier, 0.01f, "Hit 속도 배율 파라미터가 기대값과 다릅니다."); + + float startNormalizedTime = animator.GetCurrentAnimatorStateInfo(0).normalizedTime; + yield return new WaitForSeconds(AnimationSampleWindow); + float endNormalizedTime = animator.GetCurrentAnimatorStateInfo(0).normalizedTime; + + setMeasuredDelta(endNormalizedTime - startNormalizedTime); + yield return null; + } + + private IEnumerator WaitForHitState() + { + int hitStateHash = Animator.StringToHash(HitStateName); + + for (int frame = 0; frame < 120; frame++) + { + AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0); + if (!animator.IsInTransition(0) && stateInfo.shortNameHash == hitStateHash) + { + yield break; + } + + yield return null; + } + + Assert.Fail("Hit 상태 진입을 시간 내에 확인하지 못했습니다."); + } + + private static ushort AllocateTestPort() + { + return (ushort)Random.Range(20000, 40000); + } + + private void CleanupTestObjects() + { + if (playerObject != null) + { + Object.DestroyImmediate(playerObject); + playerObject = null; + } + + foreach (Camera camera in Object.FindObjectsByType(FindObjectsSortMode.None)) + { + if (camera != null && camera.gameObject.name == "PlayerCamera") + { + Object.DestroyImmediate(camera.gameObject); + } + } + + if (networkManagerObject != null) + { + Object.DestroyImmediate(networkManagerObject); + networkManagerObject = null; + networkManager = null; + } + } + } +} diff --git a/Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs.meta b/Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs.meta new file mode 100644 index 00000000..a68de562 --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/HitReactionAnimationSpeedTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9ad1b0615e5d4a13da258e727d9ef00a \ No newline at end of file diff --git a/Assets/_Game/Tests/PlayMode/Resources.meta b/Assets/_Game/Tests/PlayMode/Resources.meta new file mode 100644 index 00000000..62fd35da --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/Resources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 16e8a9b532e7b9d9ea9323e4c1f668c3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller b/Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller new file mode 100644 index 00000000..2d4f47fe --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller @@ -0,0 +1,804 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1102 &-7908033645098541312 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Idle + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: 3552691702041008167} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: ebc8e8e59d375104babccd596aa3bdc0, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1102 &-7325123755187262477 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Hit + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: -4328548395604843749} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 1 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: -4467167322993697325, guid: 575336e929435dc4398adefbfa9cde78, type: 3} + m_Tag: + m_SpeedParameter: HitSpeedMultiplier + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1101 &-5485275494767118128 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7908033645098541312} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0 + m_TransitionOffset: 0 + m_ExitTime: 0 + m_HasExitTime: 1 + m_HasFixedDuration: 0 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1107 &-5279407469294999195 +AnimatorStateMachine: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Base Layer + m_ChildStates: + - serializedVersion: 1 + m_State: {fileID: -7908033645098541312} + m_Position: {x: 30, y: 180, z: 0} + - serializedVersion: 1 + m_State: {fileID: -585817098472771744} + m_Position: {x: 30, y: 270, z: 0} + - serializedVersion: 1 + m_State: {fileID: -4652545120162758660} + m_Position: {x: 290, y: 180, z: 0} + - serializedVersion: 1 + m_State: {fileID: 7693173606830535998} + m_Position: {x: -210, y: 180, z: 0} + - serializedVersion: 1 + m_State: {fileID: 580927713284550422} + m_Position: {x: 30, y: 350, z: 0} + - serializedVersion: 1 + m_State: {fileID: -2487449162152911812} + m_Position: {x: -210, y: 350, z: 0} + - serializedVersion: 1 + m_State: {fileID: -1032254959699123210} + m_Position: {x: -450, y: 270, z: 0} + - serializedVersion: 1 + m_State: {fileID: 1111345123456789012} + m_Position: {x: -700, y: 140, z: 0} + - serializedVersion: 1 + m_State: {fileID: 1111345123456789013} + m_Position: {x: -980, y: 140, z: 0} + - serializedVersion: 1 + m_State: {fileID: 1111345123456789014} + m_Position: {x: -1260, y: 140, z: 0} + - serializedVersion: 1 + m_State: {fileID: -7325123755187262477} + m_Position: {x: -450, y: 60, z: 0} + m_ChildStateMachines: [] + m_AnyStateTransitions: + - {fileID: -754003289131015157} + - {fileID: 469741948129995159} + - {fileID: 6228136561094308872} + - {fileID: 1111345123456789021} + - {fileID: -4832749926410887626} + m_EntryTransitions: [] + m_StateMachineTransitions: {} + m_StateMachineBehaviours: [] + m_AnyStatePosition: {x: -190, y: 270, z: 0} + m_EntryPosition: {x: 50, y: 120, z: 0} + m_ExitPosition: {x: 280, y: 270, z: 0} + m_ParentStateMachinePosition: {x: 800, y: 20, z: 0} + m_DefaultState: {fileID: -7908033645098541312} +--- !u!1101 &-4832749926410887626 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Hit + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7325123755187262477} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0.75 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1102 &-4652545120162758660 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Move + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: 8997152603305261974} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: -1886843759694807048} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1101 &-4328548395604843749 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7908033645098541312} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0.95 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!114 &-2635712880128095960 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 372818b6c3ad2c3028f7411ec532d127, type: 3} + m_Name: + m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerDownRecoverExitBehaviour +--- !u!1102 &-2487449162152911812 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Skill + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: [] + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 0a0bccbcb0672f948a287f14df7b9494, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!206 &-1886843759694807048 +BlendTree: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Blend Tree + m_Childs: + - serializedVersion: 2 + m_Motion: {fileID: 4441030841997727119, guid: 005ddab128d7eb141b25099c0eb1ed39, type: 3} + m_Threshold: 0 + m_Position: {x: 0, y: 0} + m_TimeScale: 1 + m_CycleOffset: 0 + m_DirectBlendParameter: Speed + m_Mirror: 0 + - serializedVersion: 2 + m_Motion: {fileID: 2571081563578608591, guid: e6a2aa1552aac6f46b40192ec605d57e, type: 3} + m_Threshold: 0.5 + m_Position: {x: 0, y: 0} + m_TimeScale: 1 + m_CycleOffset: 0 + m_DirectBlendParameter: Speed + m_Mirror: 0 + - serializedVersion: 2 + m_Motion: {fileID: 1827226128182048838, guid: 60fb511bc6259b745b432aea6a222303, type: 3} + m_Threshold: 1 + m_Position: {x: 0, y: 0} + m_TimeScale: 1 + m_CycleOffset: 0 + m_DirectBlendParameter: Speed + m_Mirror: 0 + m_BlendParameter: Speed + m_BlendParameterY: Speed + m_MinThreshold: 0 + m_MaxThreshold: 1 + m_UseAutomaticThresholds: 1 + m_NormalizedBlendValues: 0 + m_BlendType: 0 +--- !u!1102 &-1032254959699123210 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Die + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: [] + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 0063c4809c39f4a4fab4118d94af905a, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1101 &-754003289131015157 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Jump + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -585817098472771744} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.75 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1101 &-711300537235094109 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: 580927713284550422} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.423077 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1102 &-585817098472771744 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Jump + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: -711300537235094109} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 1ec316f7bc59114438737162c529e859, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!91 &9100000 +AnimatorController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: AC_Player_Default + serializedVersion: 5 + m_AnimatorParameters: + - m_Name: Speed + m_Type: 1 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: IsGrounded + m_Type: 4 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: Jump + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: Land + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: Die + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: Down + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: Recover + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: Hit + m_Type: 9 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + - m_Name: HitSpeedMultiplier + m_Type: 1 + m_DefaultFloat: 0 + m_DefaultInt: 0 + m_DefaultBool: 0 + m_Controller: {fileID: 9100000} + m_AnimatorLayers: + - serializedVersion: 5 + m_Name: Base Layer + m_StateMachine: {fileID: -5279407469294999195} + m_Mask: {fileID: 0} + m_Motions: [] + m_Behaviours: [] + m_BlendingMode: 0 + m_SyncedLayerIndex: -1 + m_DefaultWeight: 0 + m_IKPass: 0 + m_SyncedLayerAffectsTiming: 0 + m_Controller: {fileID: 9100000} +--- !u!1101 &469741948129995159 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Land + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: 7693173606830535998} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.75 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1102 &580927713284550422 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: InAir + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: [] + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 92048c8715663824db12edf9e8f37b1a, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1102 &1111345123456789012 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: DownBegin + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: 1111345123456789022} + m_StateMachineBehaviours: + - {fileID: 2761124885249802742} + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: ca8cea0da5fabb74d9e4f0d4b31262a1, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1102 &1111345123456789013 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: DownLoop + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: 1111345123456789023} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 1a78e440e35d67c4bb4cf734a52e003a, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1102 &1111345123456789014 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Recover + m_Speed: 1 + m_CycleOffset: 0 + m_Transitions: + - {fileID: 1111345123456789024} + m_StateMachineBehaviours: + - {fileID: -2635712880128095960} + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: fce016eeacf5d20478d6d1a799f93c8f, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1101 &1111345123456789021 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Down + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: 1111345123456789012} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1101 &1111345123456789022 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: 1111345123456789013} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0.95 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1101 &1111345123456789023 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Recover + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: 1111345123456789014} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.05 + m_TransitionOffset: 0 + m_ExitTime: 0 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1101 &1111345123456789024 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: [] + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7908033645098541312} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.1 + m_TransitionOffset: 0 + m_ExitTime: 0.95 + m_HasExitTime: 1 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!114 &2761124885249802742 +MonoBehaviour: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 05c1f0fd1467993a7992e73162aebccc, type: 3} + m_Name: + m_EditorClassIdentifier: Colosseum.Game::Colosseum.Player.PlayerDownBeginExitBehaviour +--- !u!1101 &3552691702041008167 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 3 + m_ConditionEvent: Speed + m_EventTreshold: 0.01 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -4652545120162758660} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.75 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1101 &6228136561094308872 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 1 + m_ConditionEvent: Die + m_EventTreshold: 0 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -1032254959699123210} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.25 + m_TransitionOffset: 0 + m_ExitTime: 0.75 + m_HasExitTime: 0 + m_HasFixedDuration: 1 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 +--- !u!1102 &7693173606830535998 +AnimatorState: + serializedVersion: 6 + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Land + m_Speed: 3 + m_CycleOffset: 0 + m_Transitions: + - {fileID: -5485275494767118128} + m_StateMachineBehaviours: [] + m_Position: {x: 50, y: 50, z: 0} + m_IKOnFeet: 0 + m_WriteDefaultValues: 1 + m_Mirror: 0 + m_SpeedParameterActive: 0 + m_MirrorParameterActive: 0 + m_CycleOffsetParameterActive: 0 + m_TimeParameterActive: 0 + m_Motion: {fileID: 7400000, guid: 1ec316f7bc59114438737162c529e859, type: 2} + m_Tag: + m_SpeedParameter: + m_MirrorParameter: + m_CycleOffsetParameter: + m_TimeParameter: +--- !u!1101 &8997152603305261974 +AnimatorStateTransition: + m_ObjectHideFlags: 1 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_Conditions: + - m_ConditionMode: 4 + m_ConditionEvent: Speed + m_EventTreshold: 0.03 + m_DstStateMachine: {fileID: 0} + m_DstState: {fileID: -7908033645098541312} + m_Solo: 0 + m_Mute: 0 + m_IsExit: 0 + serializedVersion: 3 + m_TransitionDuration: 0.1 + m_TransitionOffset: 0 + m_ExitTime: 0.9977373 + m_HasExitTime: 0 + m_HasFixedDuration: 0 + m_InterruptionSource: 0 + m_OrderedInterruption: 1 + m_CanTransitionToSelf: 1 diff --git a/Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller.meta b/Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller.meta new file mode 100644 index 00000000..1696bad1 --- /dev/null +++ b/Assets/_Game/Tests/PlayMode/Resources/AC_Player_Default_PlayModeTest.controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b6700e3b25d1cc5fc83baea2207cb152 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 0 + userData: + assetBundleName: + assetBundleVariant: