feat: 플레이어 경직/다운 회복 구간 추가
- HitReactionController에 경직, 다운 회복 가능 구간, Hit 속도 배율 파라미터 처리를 추가 - DownBegin/Recover 상태 종료를 StateMachineBehaviour로 받아 구르기 허용 구간과 다운 해제를 분리 - 드로그 발구르기를 경직 이펙트로 전환하고 넉백/경직 이펙트에서 Hit 애니메이션 속도 배율을 설정 가능하게 정리 - PlayMode 테스트를 추가해 경직/넉백이 Hit 애니메이션 속도 배율을 실제로 반영하는지 자동 검증
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 범위 경직 효과를 생성하거나 갱신합니다.
|
||||
/// </summary>
|
||||
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<StaggerEffect>(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);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// 플레이어의 피격 제어 상태를 관리합니다.
|
||||
/// 넉백 강제 이동과 다운 상태, 피격 애니메이션 재생을 담당합니다.
|
||||
/// 경직, 넉백, 다운 상태와 피격 애니메이션 재생을 담당합니다.
|
||||
/// </summary>
|
||||
[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<bool> isDowned = new NetworkVariable<bool>(false);
|
||||
private readonly NetworkVariable<bool> isDownRecoverable = new NetworkVariable<bool>(false);
|
||||
private readonly NetworkVariable<bool> isKnockbackActive = new NetworkVariable<bool>(false);
|
||||
private readonly NetworkVariable<bool> isStaggered = new NetworkVariable<bool>(false);
|
||||
|
||||
private float downRemainingTime;
|
||||
private float downRecoverableDelayRemaining = -1f;
|
||||
private float knockbackRemainingTime;
|
||||
private float staggerRemainingTime;
|
||||
private bool isDownRecoveryAnimating;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태 여부
|
||||
/// </summary>
|
||||
public bool IsDowned => isDowned.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 중 구르기 가능 구간 여부
|
||||
/// </summary>
|
||||
public bool IsDownRecoverable => isDownRecoverable.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 넉백 강제 이동 진행 여부
|
||||
/// </summary>
|
||||
public bool IsKnockbackActive => playerMovement != null && playerMovement.IsForcedMoving;
|
||||
public bool IsKnockbackActive => isKnockbackActive.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 경직 상태 여부
|
||||
/// </summary>
|
||||
public bool IsStaggered => isStaggered.Value;
|
||||
|
||||
/// <summary>
|
||||
/// 피격 반응 무시 상태 여부
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 넉백을 적용합니다.
|
||||
/// 경직을 적용합니다.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 넉백을 적용합니다.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태를 해제합니다.
|
||||
/// DownBegin 종료 시점을 전달받아 구르기 가능 타이머를 시작합니다.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recover 상태가 끝났을 때 다운 상태를 최종 해제합니다.
|
||||
/// </summary>
|
||||
public void NotifyDownRecoverAnimationExited()
|
||||
{
|
||||
if (!IsServer || !isDowned.Value || !isDownRecoveryAnimating)
|
||||
return;
|
||||
|
||||
ClearDownState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다운 회복 가능 구간에서 구르기를 사용하며 다운 상태를 종료합니다.
|
||||
/// </summary>
|
||||
public bool TryConsumeDownRecoverableEvade()
|
||||
{
|
||||
if (!IsServer || !isDowned.Value || !isDownRecoverable.Value)
|
||||
return false;
|
||||
|
||||
ClearDownState();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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<Animator>();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ namespace Colosseum.Player
|
||||
GUILayout.Label($"사망 상태: {(networkController != null && networkController.IsDead ? "Dead" : "Alive")}");
|
||||
if (TryGetComponent<PlayerActionState>(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");
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -38,11 +38,26 @@ namespace Colosseum.Player
|
||||
/// </summary>
|
||||
public bool IsStunned => abnormalityManager != null && abnormalityManager.IsStunned;
|
||||
|
||||
/// <summary>
|
||||
/// 경직 상태 여부
|
||||
/// </summary>
|
||||
public bool IsStaggered => hitReactionController != null && hitReactionController.IsStaggered;
|
||||
|
||||
/// <summary>
|
||||
/// 넉백 상태 여부
|
||||
/// </summary>
|
||||
public bool IsKnockbackActive => hitReactionController != null && hitReactionController.IsKnockbackActive;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 상태 여부
|
||||
/// </summary>
|
||||
public bool IsDowned => hitReactionController != null && hitReactionController.IsDowned;
|
||||
|
||||
/// <summary>
|
||||
/// 다운 중 구르기 가능 구간 여부
|
||||
/// </summary>
|
||||
public bool IsDownRecoverable => hitReactionController != null && hitReactionController.IsDownRecoverable;
|
||||
|
||||
/// <summary>
|
||||
/// 침묵 상태 여부
|
||||
/// </summary>
|
||||
@@ -76,17 +91,28 @@ namespace Colosseum.Player
|
||||
/// <summary>
|
||||
/// 플레이어가 직접 이동 입력을 사용할 수 있는지 여부
|
||||
/// </summary>
|
||||
public bool CanMove => CanReceiveInput && !IsStunned && !IsDowned && !BlocksMovementForCurrentSkill();
|
||||
public bool CanMove => CanReceiveInput && !IsStunned && !IsStaggered && !IsKnockbackActive && !IsDowned && !BlocksMovementForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 점프 가능 여부
|
||||
/// </summary>
|
||||
public bool CanJump => CanReceiveInput && !IsStunned && !IsDowned && !BlocksJumpForCurrentSkill();
|
||||
public bool CanJump => CanReceiveInput && !IsStunned && !IsStaggered && !IsKnockbackActive && !IsDowned && !BlocksJumpForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 일반 스킬 시작 가능 여부
|
||||
/// </summary>
|
||||
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsDowned && !IsSilenced && !BlocksSkillUseForCurrentSkill();
|
||||
public bool CanUseSkills => CanReceiveInput && !IsStunned && !IsStaggered && !IsKnockbackActive && !IsDowned && !IsSilenced && !BlocksSkillUseForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 회피 스킬 시작 가능 여부
|
||||
/// </summary>
|
||||
public bool CanEvade => CanReceiveInput
|
||||
&& !IsStunned
|
||||
&& !IsStaggered
|
||||
&& !IsKnockbackActive
|
||||
&& !IsSilenced
|
||||
&& (!IsDowned || IsDownRecoverable)
|
||||
&& !BlocksSkillUseForCurrentSkill();
|
||||
|
||||
/// <summary>
|
||||
/// 특정 스킬의 시작 가능 여부.
|
||||
@@ -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;
|
||||
|
||||
19
Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs
Normal file
19
Assets/_Game/Scripts/Player/PlayerDownBeginExitBehaviour.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// DownBegin 상태 종료를 HitReactionController에 전달합니다.
|
||||
/// </summary>
|
||||
public class PlayerDownBeginExitBehaviour : StateMachineBehaviour
|
||||
{
|
||||
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
|
||||
{
|
||||
if (animator == null)
|
||||
return;
|
||||
|
||||
HitReactionController hitReactionController = animator.GetComponentInParent<HitReactionController>();
|
||||
hitReactionController?.NotifyDownBeginExited();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 05c1f0fd1467993a7992e73162aebccc
|
||||
@@ -0,0 +1,19 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Colosseum.Player
|
||||
{
|
||||
/// <summary>
|
||||
/// Recover 상태 종료를 HitReactionController에 전달합니다.
|
||||
/// </summary>
|
||||
public class PlayerDownRecoverExitBehaviour : StateMachineBehaviour
|
||||
{
|
||||
public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
|
||||
{
|
||||
if (animator == null)
|
||||
return;
|
||||
|
||||
HitReactionController hitReactionController = animator.GetComponentInParent<HitReactionController>();
|
||||
hitReactionController?.NotifyDownRecoverAnimationExited();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 372818b6c3ad2c3028f7411ec532d127
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
44
Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs
Normal file
44
Assets/_Game/Scripts/Skills/Effects/StaggerEffect.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Player;
|
||||
|
||||
namespace Colosseum.Skills.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// 대상에게 제자리 경직을 적용하는 스킬 효과입니다.
|
||||
/// </summary>
|
||||
[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<HitReactionController>();
|
||||
if (hitReactionController == null)
|
||||
hitReactionController = target.GetComponentInParent<HitReactionController>();
|
||||
|
||||
if (hitReactionController == null)
|
||||
{
|
||||
Debug.LogWarning($"[StaggerEffect] HitReactionController not found on target: {target.name}");
|
||||
return;
|
||||
}
|
||||
|
||||
hitReactionController.ApplyStagger(duration, playHitAnimation, hitAnimationSpeedMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 675a8e51dce8ee00db904424ae5c8d9d
|
||||
@@ -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>();
|
||||
hitReactionController?.TryConsumeDownRecoverableEvade();
|
||||
}
|
||||
|
||||
currentLoadoutEntry = loadoutEntry != null ? loadoutEntry.CreateCopy() : SkillLoadoutEntry.CreateTemporary(skill);
|
||||
currentSkill = skill;
|
||||
lastCancelReason = SkillCancelReason.None;
|
||||
|
||||
@@ -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));
|
||||
/// <summary>
|
||||
/// 순차 재생할 클립 목록입니다.
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user