feat: 피격 반응 면역을 경직/넉백/다운으로 분리

- 상태이상 데이터와 관리자에서 단일 피격 면역을 경직, 넉백, 다운 개별 면역으로 분리

- 플레이어 프리팹과 디버그 메뉴, 공용 경직 애니메이션을 갱신해 분리된 면역 상태를 테스트 가능하게 정리

- PlayMode 테스트를 추가해 각 면역이 대응하는 반응만 차단하는지 검증
This commit is contained in:
2026-04-09 23:22:02 +09:00
parent 7776f7ed05
commit 0fa23d4389
17 changed files with 42220 additions and 32 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c23cdc1de7e1def5b30d36847b8626e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 7400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -19,7 +19,9 @@ MonoBehaviour:
isDebuff: 0 isDebuff: 0
showInUI: 1 showInUI: 1
isShieldState: 1 isShieldState: 1
ignoreHitReaction: 0 ignoreStagger: 0
ignoreKnockback: 0
ignoreDown: 0
loopingVfxPrefab: {fileID: 0} loopingVfxPrefab: {fileID: 0}
loopingVfxOffset: {x: 0, y: 0, z: 0} loopingVfxOffset: {x: 0, y: 0, z: 0}
loopingVfxScaleMultiplier: 1 loopingVfxScaleMultiplier: 1

View File

@@ -18,7 +18,9 @@ MonoBehaviour:
level: 1 level: 1
isDebuff: 0 isDebuff: 0
showInUI: 0 showInUI: 0
ignoreHitReaction: 0 ignoreStagger: 0
ignoreKnockback: 0
ignoreDown: 0
loopingVfxPrefab: {fileID: 1800972780968652, guid: 205d983549fba2a47a7808abf228f4be, type: 3} loopingVfxPrefab: {fileID: 1800972780968652, guid: 205d983549fba2a47a7808abf228f4be, type: 3}
loopingVfxOffset: {x: 0, y: 3.2, z: 0} loopingVfxOffset: {x: 0, y: 3.2, z: 0}
loopingVfxScaleMultiplier: 6 loopingVfxScaleMultiplier: 6

View File

@@ -18,7 +18,9 @@ MonoBehaviour:
level: 1 level: 1
isDebuff: 0 isDebuff: 0
showInUI: 0 showInUI: 0
ignoreHitReaction: 1 ignoreStagger: 1
ignoreKnockback: 0
ignoreDown: 0
statModifiers: [] statModifiers: []
periodicInterval: 0 periodicInterval: 0
periodicValue: 0 periodicValue: 0

View File

@@ -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: b08cc671f858a3b409170a5356e960a0, type: 3}
m_Name: Data_Abnormality_Player_넉백면역
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityData
abnormalityName: 넉백 면역
icon: {fileID: 0}
duration: 0.6
level: 1
isDebuff: 0
showInUI: 0
ignoreStagger: 0
ignoreKnockback: 1
ignoreDown: 0
statModifiers: []
periodicInterval: 0
periodicValue: 0
controlType: 0
slowMultiplier: 0.5
incomingDamageMultiplier: 1

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 13796a48dd6e48a8a391c1b8d25e7020

View File

@@ -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: b08cc671f858a3b409170a5356e960a0, type: 3}
m_Name: Data_Abnormality_Player_다운면역
m_EditorClassIdentifier: Colosseum.Game::Colosseum.Abnormalities.AbnormalityData
abnormalityName: 다운 면역
icon: {fileID: 0}
duration: 0.6
level: 1
isDebuff: 0
showInUI: 0
ignoreStagger: 0
ignoreKnockback: 0
ignoreDown: 1
statModifiers: []
periodicInterval: 0
periodicValue: 0
controlType: 0
slowMultiplier: 0.5
incomingDamageMultiplier: 1

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: cc4faad006db4769b2400d8b8a8a2706

View File

@@ -19,7 +19,9 @@ MonoBehaviour:
isDebuff: 0 isDebuff: 0
showInUI: 1 showInUI: 1
isShieldState: 1 isShieldState: 1
ignoreHitReaction: 0 ignoreStagger: 0
ignoreKnockback: 0
ignoreDown: 0
loopingVfxPrefab: {fileID: 0} loopingVfxPrefab: {fileID: 0}
loopingVfxOffset: {x: 0, y: 0, z: 0} loopingVfxOffset: {x: 0, y: 0, z: 0}
loopingVfxScaleMultiplier: 1 loopingVfxScaleMultiplier: 1

View File

@@ -19,7 +19,9 @@ MonoBehaviour:
isDebuff: 0 isDebuff: 0
showInUI: 1 showInUI: 1
isShieldState: 1 isShieldState: 1
ignoreHitReaction: 0 ignoreStagger: 0
ignoreKnockback: 0
ignoreDown: 0
loopingVfxPrefab: {fileID: 0} loopingVfxPrefab: {fileID: 0}
loopingVfxOffset: {x: 0, y: 0, z: 0} loopingVfxOffset: {x: 0, y: 0, z: 0}
loopingVfxScaleMultiplier: 1 loopingVfxScaleMultiplier: 1

View File

@@ -4726,6 +4726,7 @@ GameObject:
- component: {fileID: 2540460367028266762} - component: {fileID: 2540460367028266762}
- component: {fileID: 1829782337872253002} - component: {fileID: 1829782337872253002}
- component: {fileID: 7647416756581242399} - component: {fileID: 7647416756581242399}
- component: {fileID: -263312875943034690}
m_Layer: 0 m_Layer: 0
m_Name: Prefab_Player_Default m_Name: Prefab_Player_Default
m_TagString: Player m_TagString: Player
@@ -4903,6 +4904,7 @@ MonoBehaviour:
ShowTopMostFoldoutHeaderGroup: 1 ShowTopMostFoldoutHeaderGroup: 1
characterStats: {fileID: -5132198055668300151} characterStats: {fileID: -5132198055668300151}
abnormalityManager: {fileID: 0} abnormalityManager: {fileID: 0}
defenseController: {fileID: 0}
shieldStateAbnormality: {fileID: 0} shieldStateAbnormality: {fileID: 0}
passivePrototypeCatalog: {fileID: 11400000, guid: 328a12c12954ef649a9ba5892323c345, type: 2} passivePrototypeCatalog: {fileID: 11400000, guid: 328a12c12954ef649a9ba5892323c345, type: 2}
passiveTree: {fileID: 11400000, guid: 33ad68732732b6a4589e24da88253f8d, type: 2} passiveTree: {fileID: 11400000, guid: 33ad68732732b6a4589e24da88253f8d, type: 2}
@@ -4965,6 +4967,14 @@ MonoBehaviour:
NameHash: 976603345 NameHash: 976603345
Synchronize: 1 Synchronize: 1
ParameterType: 9 ParameterType: 9
- name: Hit
NameHash: 1654612129
Synchronize: 1
ParameterType: 9
- name: HitSpeedMultiplier
NameHash: -316320400
Synchronize: 1
ParameterType: 1
AnimatorParametersExpanded: 0 AnimatorParametersExpanded: 0
--- !u!95 &3426985706796420257 --- !u!95 &3426985706796420257
Animator: Animator:
@@ -5026,19 +5036,29 @@ MonoBehaviour:
registeredClips: registeredClips:
- {fileID: 7400000, guid: e920395a39d50ca429748ac26967e22f, type: 2} - {fileID: 7400000, guid: e920395a39d50ca429748ac26967e22f, type: 2}
- {fileID: 7400000, guid: 920ea8a73bbf84849b01d3875ff4e4c3, type: 2} - {fileID: 7400000, guid: 920ea8a73bbf84849b01d3875ff4e4c3, type: 2}
- {fileID: 7400000, guid: 0e2ed792a5988304c82e86a671d181d9, type: 2}
- {fileID: 7400000, guid: 5c93f4c7dbc1b314d8e7b318826ef530, type: 2} - {fileID: 7400000, guid: 5c93f4c7dbc1b314d8e7b318826ef530, type: 2}
- {fileID: 7400000, guid: afba3dfa5c315c04fa8af0e2e52aee3c, type: 2} - {fileID: 7400000, guid: afba3dfa5c315c04fa8af0e2e52aee3c, type: 2}
- {fileID: 7400000, guid: d8bb58f4f1cc36440896fec5c4002e59, type: 2} - {fileID: 7400000, guid: d8bb58f4f1cc36440896fec5c4002e59, type: 2}
- {fileID: 7400000, guid: dc5e72498abe07149b02d93a308697db, type: 2} - {fileID: 7400000, guid: dc5e72498abe07149b02d93a308697db, type: 2}
- {fileID: 7400000, guid: 7ba0436909058c54fb6203180daf8377, type: 2}
- {fileID: 7400000, guid: 53d4b641824bfd248ab6ba10ec9c4b00, type: 2} - {fileID: 7400000, guid: 53d4b641824bfd248ab6ba10ec9c4b00, type: 2}
- {fileID: 7400000, guid: 81732d3b50fdd8645b9465a4f12a99ae, type: 2}
- {fileID: 7400000, guid: 55409975b480666448b276c21d9aeb3d, type: 2}
- {fileID: 7400000, guid: 686713d64f5e0dd4585b6a3d00fb25a3, type: 2}
- {fileID: 7400000, guid: fabf33afcfd658d47a5566582fdbf044, type: 2} - {fileID: 7400000, guid: fabf33afcfd658d47a5566582fdbf044, type: 2}
- {fileID: 7400000, guid: 581f9de1a3e502f4eb95b1e922117c5c, type: 2} - {fileID: 7400000, guid: 581f9de1a3e502f4eb95b1e922117c5c, type: 2}
- {fileID: 7400000, guid: 60a7aefb54ade4a41904977685930f3d, type: 2}
- {fileID: 7400000, guid: b08df23bb6e5efe4db4dae2bb02382b1, type: 2} - {fileID: 7400000, guid: b08df23bb6e5efe4db4dae2bb02382b1, type: 2}
- {fileID: 7400000, guid: 3fe6587798a1a3a47a568b0dea6ffbf4, type: 2}
- {fileID: 7400000, guid: 448433a9cacf21e428d4e29f81741ad2, type: 2}
- {fileID: 7400000, guid: e586d02e6719b56489738b82604abdcf, type: 2} - {fileID: 7400000, guid: e586d02e6719b56489738b82604abdcf, type: 2}
- {fileID: 7400000, guid: 0c9ba11ed23236649929b0225a8beadb, type: 2} - {fileID: 7400000, guid: 0c9ba11ed23236649929b0225a8beadb, type: 2}
- {fileID: 7400000, guid: fe55ab7fad6cd644db9bd2b6183fe180, type: 2}
- {fileID: 7400000, guid: 7813457eddc067249a6ed50f31b6fe8c, type: 2} - {fileID: 7400000, guid: 7813457eddc067249a6ed50f31b6fe8c, type: 2}
- {fileID: 7400000, guid: b7223eada9e64304baa6b4c9dcdfcd15, type: 2} - {fileID: 7400000, guid: b7223eada9e64304baa6b4c9dcdfcd15, type: 2}
- {fileID: 7400000, guid: 295669009c1870f4ab5d43fa28fd6e30, type: 2} - {fileID: 7400000, guid: 295669009c1870f4ab5d43fa28fd6e30, type: 2}
- {fileID: 7400000, guid: e7ef39b0eacfc6940a8c7e1f20ba97e7, type: 2}
- {fileID: 7400000, guid: 7f0d64090ab5adf439b9437c98a0bdd0, type: 2} - {fileID: 7400000, guid: 7f0d64090ab5adf439b9437c98a0bdd0, type: 2}
- {fileID: 7400000, guid: df8ffc011feaf17429b7f429f635db3f, type: 2} - {fileID: 7400000, guid: df8ffc011feaf17429b7f429f635db3f, type: 2}
- {fileID: 7400000, guid: 23aba6e7f76aa57478374325cb4f454e, type: 2} - {fileID: 7400000, guid: 23aba6e7f76aa57478374325cb4f454e, type: 2}
@@ -5047,6 +5067,7 @@ MonoBehaviour:
debugDrawDuration: 1 debugDrawDuration: 1
lastCancelledSkillName: lastCancelledSkillName:
lastCancelReason: 0 lastCancelReason: 0
lastExecutionResult: 0
--- !u!114 &6585367215453362640 --- !u!114 &6585367215453362640
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -5208,6 +5229,7 @@ MonoBehaviour:
abnormalityManager: {fileID: 0} abnormalityManager: {fileID: 0}
skillController: {fileID: 0} skillController: {fileID: 0}
hitReactionController: {fileID: 0} hitReactionController: {fileID: 0}
defenseController: {fileID: 0}
spectator: {fileID: 0} spectator: {fileID: 0}
--- !u!114 &2540460367028266762 --- !u!114 &2540460367028266762
MonoBehaviour: MonoBehaviour:
@@ -5281,6 +5303,22 @@ MonoBehaviour:
inRangeColor: {r: 0.2, g: 1, b: 0.2, a: 0.8} inRangeColor: {r: 0.2, g: 1, b: 0.2, a: 0.8}
outOfRangeColor: {r: 1, g: 0.3, b: 0.3, a: 0.8} outOfRangeColor: {r: 1, g: 0.3, b: 0.3, a: 0.8}
useSkillRange: 1 useSkillRange: 1
--- !u!208 &-263312875943034690
NavMeshObstacle:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6473031571298860035}
m_Enabled: 0
serializedVersion: 3
m_Shape: 1
m_Extents: {x: 0.5, y: 0.5, z: 0.5}
m_MoveThreshold: 0.1
m_Carve: 0
m_CarveOnlyStationary: 1
m_Center: {x: 0, y: 0, z: 0}
m_TimeToStationary: 0.5
--- !u!1 &6541621058422482552 --- !u!1 &6541621058422482552
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

View File

@@ -71,8 +71,14 @@ namespace Colosseum.Abnormalities
[Tooltip("보호막 계열 상태인지 여부 (보호막 인스턴스 동기화용)")] [Tooltip("보호막 계열 상태인지 여부 (보호막 인스턴스 동기화용)")]
public bool isShieldState = false; public bool isShieldState = false;
[Tooltip("활성 중에는 일반 피격 반응(경직, 넉백, 다운)을 무시할지 여부")] [Tooltip("활성 중에는 경직을 무시할지 여부")]
public bool ignoreHitReaction = false; public bool ignoreStagger = false;
[Tooltip("활성 중에는 넉백을 무시할지 여부")]
public bool ignoreKnockback = false;
[Tooltip("활성 중에는 다운을 무시할지 여부")]
public bool ignoreDown = false;
[Header("시각 효과")] [Header("시각 효과")]
[Tooltip("이상 상태가 유지되는 동안 대상에 붙일 루핑 VFX 프리팹")] [Tooltip("이상 상태가 유지되는 동안 대상에 붙일 루핑 VFX 프리팹")]

View File

@@ -32,7 +32,9 @@ namespace Colosseum.Abnormalities
private int stunCount; private int stunCount;
private int silenceCount; private int silenceCount;
private int invincibleCount; private int invincibleCount;
private int hitReactionImmuneCount; private int staggerImmuneCount;
private int knockbackImmuneCount;
private int downImmuneCount;
private float slowMultiplier = 1f; private float slowMultiplier = 1f;
private float incomingDamageMultiplier = 1f; private float incomingDamageMultiplier = 1f;
@@ -40,7 +42,9 @@ namespace Colosseum.Abnormalities
private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0); private NetworkVariable<int> syncedStunCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedSilenceCount = new NetworkVariable<int>(0); private NetworkVariable<int> syncedSilenceCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedInvincibleCount = new NetworkVariable<int>(0); private NetworkVariable<int> syncedInvincibleCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedHitReactionImmuneCount = new NetworkVariable<int>(0); private NetworkVariable<int> syncedStaggerImmuneCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedKnockbackImmuneCount = new NetworkVariable<int>(0);
private NetworkVariable<int> syncedDownImmuneCount = new NetworkVariable<int>(0);
private NetworkVariable<float> syncedSlowMultiplier = new NetworkVariable<float>(1f); private NetworkVariable<float> syncedSlowMultiplier = new NetworkVariable<float>(1f);
// 네트워크 동기화용 데이터 // 네트워크 동기화용 데이터
@@ -62,9 +66,19 @@ namespace Colosseum.Abnormalities
public bool IsInvincible => GetCurrentInvincibleCount() > 0; public bool IsInvincible => GetCurrentInvincibleCount() > 0;
/// <summary> /// <summary>
/// 일반 피격 반응 무시 상태 여부 /// 경직 면역 상태 여부
/// </summary> /// </summary>
public bool IsHitReactionImmune => GetCurrentHitReactionImmuneCount() > 0; public bool IsStaggerImmune => GetCurrentStaggerImmuneCount() > 0;
/// <summary>
/// 넉백 면역 상태 여부
/// </summary>
public bool IsKnockbackImmune => GetCurrentKnockbackImmuneCount() > 0;
/// <summary>
/// 다운 면역 상태 여부
/// </summary>
public bool IsDownImmune => GetCurrentDownImmuneCount() > 0;
/// <summary> /// <summary>
/// 이동 속도 배율 (1.0 = 기본, 0.5 = 50% 감소) /// 이동 속도 배율 (1.0 = 기본, 0.5 = 50% 감소)
@@ -139,7 +153,9 @@ namespace Colosseum.Abnormalities
syncedStunCount.OnValueChanged += HandleSyncedStunChanged; syncedStunCount.OnValueChanged += HandleSyncedStunChanged;
syncedSilenceCount.OnValueChanged += HandleSyncedSilenceChanged; syncedSilenceCount.OnValueChanged += HandleSyncedSilenceChanged;
syncedInvincibleCount.OnValueChanged += HandleSyncedInvincibleChanged; syncedInvincibleCount.OnValueChanged += HandleSyncedInvincibleChanged;
syncedHitReactionImmuneCount.OnValueChanged += HandleSyncedHitReactionImmuneChanged; syncedStaggerImmuneCount.OnValueChanged += HandleSyncedStaggerImmuneChanged;
syncedKnockbackImmuneCount.OnValueChanged += HandleSyncedKnockbackImmuneChanged;
syncedDownImmuneCount.OnValueChanged += HandleSyncedDownImmuneChanged;
syncedSlowMultiplier.OnValueChanged += HandleSyncedSlowChanged; syncedSlowMultiplier.OnValueChanged += HandleSyncedSlowChanged;
if (networkController != null) if (networkController != null)
@@ -161,7 +177,9 @@ namespace Colosseum.Abnormalities
syncedStunCount.OnValueChanged -= HandleSyncedStunChanged; syncedStunCount.OnValueChanged -= HandleSyncedStunChanged;
syncedSilenceCount.OnValueChanged -= HandleSyncedSilenceChanged; syncedSilenceCount.OnValueChanged -= HandleSyncedSilenceChanged;
syncedInvincibleCount.OnValueChanged -= HandleSyncedInvincibleChanged; syncedInvincibleCount.OnValueChanged -= HandleSyncedInvincibleChanged;
syncedHitReactionImmuneCount.OnValueChanged -= HandleSyncedHitReactionImmuneChanged; syncedStaggerImmuneCount.OnValueChanged -= HandleSyncedStaggerImmuneChanged;
syncedKnockbackImmuneCount.OnValueChanged -= HandleSyncedKnockbackImmuneChanged;
syncedDownImmuneCount.OnValueChanged -= HandleSyncedDownImmuneChanged;
syncedSlowMultiplier.OnValueChanged -= HandleSyncedSlowChanged; syncedSlowMultiplier.OnValueChanged -= HandleSyncedSlowChanged;
if (networkController != null) if (networkController != null)
@@ -447,10 +465,14 @@ namespace Colosseum.Abnormalities
{ {
bool enteredStun = false; bool enteredStun = false;
if (data.ignoreHitReaction) if (data.ignoreStagger)
{ staggerImmuneCount++;
hitReactionImmuneCount++;
} if (data.ignoreKnockback)
knockbackImmuneCount++;
if (data.ignoreDown)
downImmuneCount++;
switch (data.controlType) switch (data.controlType)
{ {
@@ -482,10 +504,14 @@ namespace Colosseum.Abnormalities
private void RemoveControlEffect(AbnormalityData data) private void RemoveControlEffect(AbnormalityData data)
{ {
if (data.ignoreHitReaction) if (data.ignoreStagger)
{ staggerImmuneCount = Mathf.Max(0, staggerImmuneCount - 1);
hitReactionImmuneCount = Mathf.Max(0, hitReactionImmuneCount - 1);
} if (data.ignoreKnockback)
knockbackImmuneCount = Mathf.Max(0, knockbackImmuneCount - 1);
if (data.ignoreDown)
downImmuneCount = Mathf.Max(0, downImmuneCount - 1);
switch (data.controlType) switch (data.controlType)
{ {
@@ -542,7 +568,11 @@ namespace Colosseum.Abnormalities
private int GetCurrentInvincibleCount() => IsServer ? invincibleCount : syncedInvincibleCount.Value; private int GetCurrentInvincibleCount() => IsServer ? invincibleCount : syncedInvincibleCount.Value;
private int GetCurrentHitReactionImmuneCount() => IsServer ? hitReactionImmuneCount : syncedHitReactionImmuneCount.Value; private int GetCurrentStaggerImmuneCount() => IsServer ? staggerImmuneCount : syncedStaggerImmuneCount.Value;
private int GetCurrentKnockbackImmuneCount() => IsServer ? knockbackImmuneCount : syncedKnockbackImmuneCount.Value;
private int GetCurrentDownImmuneCount() => IsServer ? downImmuneCount : syncedDownImmuneCount.Value;
private float GetCurrentSlowMultiplier() => IsServer ? slowMultiplier : syncedSlowMultiplier.Value; private float GetCurrentSlowMultiplier() => IsServer ? slowMultiplier : syncedSlowMultiplier.Value;
@@ -554,7 +584,9 @@ namespace Colosseum.Abnormalities
syncedStunCount.Value = stunCount; syncedStunCount.Value = stunCount;
syncedSilenceCount.Value = silenceCount; syncedSilenceCount.Value = silenceCount;
syncedInvincibleCount.Value = invincibleCount; syncedInvincibleCount.Value = invincibleCount;
syncedHitReactionImmuneCount.Value = hitReactionImmuneCount; syncedStaggerImmuneCount.Value = staggerImmuneCount;
syncedKnockbackImmuneCount.Value = knockbackImmuneCount;
syncedDownImmuneCount.Value = downImmuneCount;
syncedSlowMultiplier.Value = slowMultiplier; syncedSlowMultiplier.Value = slowMultiplier;
} }
@@ -636,7 +668,23 @@ namespace Colosseum.Abnormalities
OnAbnormalitiesChanged?.Invoke(); OnAbnormalitiesChanged?.Invoke();
} }
private void HandleSyncedHitReactionImmuneChanged(int oldValue, int newValue) private void HandleSyncedStaggerImmuneChanged(int oldValue, int newValue)
{
if (oldValue == newValue)
return;
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedKnockbackImmuneChanged(int oldValue, int newValue)
{
if (oldValue == newValue)
return;
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedDownImmuneChanged(int oldValue, int newValue)
{ {
if (oldValue == newValue) if (oldValue == newValue)
return; return;

View File

@@ -36,7 +36,7 @@ namespace Colosseum.Editor
private const string StunAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset"; private const string StunAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset";
private const string SilenceAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset"; private const string SilenceAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset";
private const string MarkAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_집행자의낙인.asset"; private const string MarkAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_집행자의낙인.asset";
private const string HitReactionImmuneAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_경직면역.asset"; private const string StaggerImmuneAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_경직면역.asset";
private const string TestDebuffAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Test_Debuff.asset"; private const string TestDebuffAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Test_Debuff.asset";
private const string ShieldAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Common_보호막.asset"; private const string ShieldAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Common_보호막.asset";
private const string ShieldTypeAPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Test_보호막A.asset"; private const string ShieldTypeAPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Test_보호막A.asset";
@@ -729,7 +729,7 @@ namespace Colosseum.Editor
SkillEffect damageEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_찌르기_0_데미지.asset"); SkillEffect damageEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_찌르기_0_데미지.asset");
SkillEffect tauntEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_도발_0_도발.asset"); SkillEffect tauntEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_도발_0_도발.asset");
SkillEffect shieldEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_보호막_0_보호막.asset"); SkillEffect shieldEffect = AssetDatabase.LoadAssetAtPath<SkillEffect>("Assets/_Game/Data/Skills/Effects/Data_SkillEffect_Player_보호막_0_보호막.asset");
AbnormalityData hitReactionImmuneAbnormality = AssetDatabase.LoadAssetAtPath<AbnormalityData>(HitReactionImmuneAbnormalityPath); AbnormalityData staggerImmuneAbnormality = AssetDatabase.LoadAssetAtPath<AbnormalityData>(StaggerImmuneAbnormalityPath);
AbnormalityData testDebuffAbnormality = AssetDatabase.LoadAssetAtPath<AbnormalityData>(TestDebuffAbnormalityPath); AbnormalityData testDebuffAbnormality = AssetDatabase.LoadAssetAtPath<AbnormalityData>(TestDebuffAbnormalityPath);
DamageEffect edgeDamageEffect = CreateOrUpdateDamageEffectAsset(EdgeDamageEffectPath, 4f); DamageEffect edgeDamageEffect = CreateOrUpdateDamageEffectAsset(EdgeDamageEffectPath, 4f);
DamageEffect impactDamageEffect = CreateOrUpdateDamageEffectAsset(ImpactDamageEffectPath, 7f); DamageEffect impactDamageEffect = CreateOrUpdateDamageEffectAsset(ImpactDamageEffectPath, 7f);
@@ -821,7 +821,7 @@ namespace Colosseum.Editor
1f, 1f,
0, 0,
null, null,
new[] { hitReactionImmuneAbnormality }, new[] { staggerImmuneAbnormality },
allowedSkillRoles: SkillRoleType.All, allowedSkillRoles: SkillRoleType.All,
allowedSkillActivationTypes: SkillActivationType.All); allowedSkillActivationTypes: SkillActivationType.All);

View File

@@ -84,9 +84,19 @@ namespace Colosseum.Player
public bool IsStaggered => isStaggered.Value; public bool IsStaggered => isStaggered.Value;
/// <summary> /// <summary>
/// 피격 반응 무시 상태 여부 /// 경직 면역 상태 여부
/// </summary> /// </summary>
public bool IsHitReactionImmune => abnormalityManager != null && abnormalityManager.IsHitReactionImmune; public bool IsStaggerImmune => abnormalityManager != null && abnormalityManager.IsStaggerImmune;
/// <summary>
/// 넉백 면역 상태 여부
/// </summary>
public bool IsKnockbackImmune => abnormalityManager != null && abnormalityManager.IsKnockbackImmune;
/// <summary>
/// 다운 면역 상태 여부
/// </summary>
public bool IsDownImmune => abnormalityManager != null && abnormalityManager.IsDownImmune;
private void Awake() private void Awake()
{ {
@@ -121,7 +131,7 @@ namespace Colosseum.Player
if (networkController != null && networkController.IsDead) if (networkController != null && networkController.IsDead)
return; return;
if (IsHitReactionImmune || isDowned.Value) if (IsStaggerImmune || isDowned.Value)
return; return;
if (duration <= 0f) if (duration <= 0f)
@@ -153,7 +163,7 @@ namespace Colosseum.Player
if (networkController != null && networkController.IsDead) if (networkController != null && networkController.IsDead)
return; return;
if (IsHitReactionImmune || isDowned.Value) if (IsKnockbackImmune || isDowned.Value)
return; return;
if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f) if (duration <= 0f || worldVelocity.sqrMagnitude <= 0.0001f)
@@ -187,7 +197,7 @@ namespace Colosseum.Player
if (networkController != null && networkController.IsDead) if (networkController != null && networkController.IsDead)
return; return;
if (IsHitReactionImmune) if (IsDownImmune)
return; return;
downRemainingTime = Mathf.Max(downRemainingTime, duration); downRemainingTime = Mathf.Max(downRemainingTime, duration);

View File

@@ -8,6 +8,7 @@ using UnityEngine.TestTools;
using Unity.Netcode; using Unity.Netcode;
using Unity.Netcode.Transports.UTP; using Unity.Netcode.Transports.UTP;
using Colosseum.Abnormalities;
using Colosseum.Player; using Colosseum.Player;
namespace Colosseum.Tests namespace Colosseum.Tests
@@ -26,6 +27,7 @@ namespace Colosseum.Tests
private NetworkManager networkManager; private NetworkManager networkManager;
private GameObject playerObject; private GameObject playerObject;
private HitReactionController hitReactionController; private HitReactionController hitReactionController;
private AbnormalityManager abnormalityManager;
private Animator animator; private Animator animator;
[UnitySetUp] [UnitySetUp]
@@ -55,6 +57,7 @@ namespace Colosseum.Tests
playerObject.transform.position = Vector3.zero; playerObject.transform.position = Vector3.zero;
playerObject.AddComponent<NetworkObject>(); playerObject.AddComponent<NetworkObject>();
abnormalityManager = playerObject.AddComponent<AbnormalityManager>();
hitReactionController = playerObject.AddComponent<HitReactionController>(); hitReactionController = playerObject.AddComponent<HitReactionController>();
GameObject visualObject = new GameObject("Visual"); GameObject visualObject = new GameObject("Visual");
@@ -72,6 +75,7 @@ namespace Colosseum.Tests
yield return null; yield return null;
Assert.NotNull(hitReactionController, "HitReactionController를 생성하지 못했습니다."); Assert.NotNull(hitReactionController, "HitReactionController를 생성하지 못했습니다.");
Assert.NotNull(abnormalityManager, "AbnormalityManager를 생성하지 못했습니다.");
Assert.NotNull(animator, "테스트용 Animator를 생성하지 못했습니다."); Assert.NotNull(animator, "테스트용 Animator를 생성하지 못했습니다.");
} }
@@ -120,6 +124,69 @@ namespace Colosseum.Tests
Assert.Greater(measuredDelta, 0.01f, "넉백에서 Hit 애니메이션이 재생되지 않았습니다."); Assert.Greater(measuredDelta, 0.01f, "넉백에서 Hit 애니메이션이 재생되지 않았습니다.");
} }
[UnityTest]
public IEnumerator StaggerImmunity_OnlyBlocksStagger()
{
AbnormalityData staggerImmunity = CreateImmunityAbnormality("테스트 경직 면역", ignoreStagger: true);
abnormalityManager.ApplyAbnormality(staggerImmunity, playerObject);
yield return null;
hitReactionController.ApplyStagger(0.4f, false);
Assert.IsFalse(hitReactionController.IsStaggered, "경직 면역 상태인데 경직이 적용되었습니다.");
yield return ResetHitReactionState();
hitReactionController.ApplyKnockback(Vector3.back * 3f, 0.25f, false);
Assert.IsTrue(hitReactionController.IsKnockbackActive, "경직 면역이 넉백까지 막으면 안 됩니다.");
yield return ResetHitReactionState();
hitReactionController.ApplyDown(0.3f);
Assert.IsTrue(hitReactionController.IsDowned, "경직 면역이 다운까지 막으면 안 됩니다.");
Object.DestroyImmediate(staggerImmunity);
}
[UnityTest]
public IEnumerator KnockbackImmunity_OnlyBlocksKnockback()
{
AbnormalityData knockbackImmunity = CreateImmunityAbnormality("테스트 넉백 면역", ignoreKnockback: true);
abnormalityManager.ApplyAbnormality(knockbackImmunity, playerObject);
yield return null;
hitReactionController.ApplyStagger(0.4f, false);
Assert.IsTrue(hitReactionController.IsStaggered, "넉백 면역이 경직까지 막으면 안 됩니다.");
yield return ResetHitReactionState();
hitReactionController.ApplyKnockback(Vector3.back * 3f, 0.25f, false);
Assert.IsFalse(hitReactionController.IsKnockbackActive, "넉백 면역 상태인데 넉백이 적용되었습니다.");
yield return ResetHitReactionState();
hitReactionController.ApplyDown(0.3f);
Assert.IsTrue(hitReactionController.IsDowned, "넉백 면역이 다운까지 막으면 안 됩니다.");
Object.DestroyImmediate(knockbackImmunity);
}
[UnityTest]
public IEnumerator DownImmunity_OnlyBlocksDown()
{
AbnormalityData downImmunity = CreateImmunityAbnormality("테스트 다운 면역", ignoreDown: true);
abnormalityManager.ApplyAbnormality(downImmunity, playerObject);
yield return null;
hitReactionController.ApplyStagger(0.4f, false);
Assert.IsTrue(hitReactionController.IsStaggered, "다운 면역이 경직까지 막으면 안 됩니다.");
yield return ResetHitReactionState();
hitReactionController.ApplyKnockback(Vector3.back * 3f, 0.25f, false);
Assert.IsTrue(hitReactionController.IsKnockbackActive, "다운 면역이 넉백까지 막으면 안 됩니다.");
yield return ResetHitReactionState();
hitReactionController.ApplyDown(0.3f);
Assert.IsFalse(hitReactionController.IsDowned, "다운 면역 상태인데 다운이 적용되었습니다.");
Object.DestroyImmediate(downImmunity);
}
private IEnumerator MeasureHitPlaybackDelta(System.Action applyReaction, float expectedSpeedMultiplier, System.Action<float> setMeasuredDelta) private IEnumerator MeasureHitPlaybackDelta(System.Action applyReaction, float expectedSpeedMultiplier, System.Action<float> setMeasuredDelta)
{ {
hitReactionController.ClearHitReactionState(); hitReactionController.ClearHitReactionState();
@@ -164,8 +231,33 @@ namespace Colosseum.Tests
return (ushort)Random.Range(20000, 40000); return (ushort)Random.Range(20000, 40000);
} }
private static AbnormalityData CreateImmunityAbnormality(string abnormalityName, bool ignoreStagger = false, bool ignoreKnockback = false, bool ignoreDown = false)
{
AbnormalityData abnormalityData = ScriptableObject.CreateInstance<AbnormalityData>();
abnormalityData.abnormalityName = abnormalityName;
abnormalityData.duration = 5f;
abnormalityData.ignoreStagger = ignoreStagger;
abnormalityData.ignoreKnockback = ignoreKnockback;
abnormalityData.ignoreDown = ignoreDown;
return abnormalityData;
}
private IEnumerator ResetHitReactionState()
{
hitReactionController.ClearHitReactionState();
yield return null;
}
private void CleanupTestObjects() private void CleanupTestObjects()
{ {
if (NetworkManager.Singleton != null && NetworkManager.Singleton.gameObject != networkManagerObject)
{
if (NetworkManager.Singleton.IsListening)
NetworkManager.Singleton.Shutdown();
Object.DestroyImmediate(NetworkManager.Singleton.gameObject);
}
if (playerObject != null) if (playerObject != null)
{ {
Object.DestroyImmediate(playerObject); Object.DestroyImmediate(playerObject);