feat: 보호막 타입 분리 및 드로그 시그니처 전조 정리
- 보호막을 단일 수치에서 타입별 독립 인스턴스 구조로 리팩터링하고 같은 타입만 갱신되도록 정리 - 플레이어/보스 보호막 상태를 이상상태와 연동해 HUD 및 보스 UI에서 타입별로 식별 가능하게 보강 - 드로그 집행 개시 전조를 집행 준비 이상상태 기반으로 재구성하고 관련 데이터와 보스 컨텍스트를 정리 - 전투 밸런스 계측기와 디버그 메뉴를 추가해 피해, 치유, 보호막, 위협, 패턴 사용량 측정 경로를 마련 - 테스트용 보호막 A/B와 시그니처 전조 자산을 추가하고 기본 포트 7777 원복 후 빌드 및 런타임 검증을 완료
This commit is contained in:
@@ -68,9 +68,26 @@ namespace Colosseum.Abnormalities
|
||||
[Tooltip("플레이어 HUD의 이상상태 UI에 표시할지 여부")]
|
||||
public bool showInUI = true;
|
||||
|
||||
[Tooltip("보호막 계열 상태인지 여부 (보호막 인스턴스 동기화용)")]
|
||||
public bool isShieldState = false;
|
||||
|
||||
[Tooltip("활성 중에는 일반 피격 반응(경직, 넉백, 다운)을 무시할지 여부")]
|
||||
public bool ignoreHitReaction = false;
|
||||
|
||||
[Header("시각 효과")]
|
||||
[Tooltip("이상 상태가 유지되는 동안 대상에 붙일 루핑 VFX 프리팹")]
|
||||
public GameObject loopingVfxPrefab;
|
||||
|
||||
[Tooltip("루핑 VFX 위치 보정값")]
|
||||
public Vector3 loopingVfxOffset = Vector3.zero;
|
||||
|
||||
[Tooltip("루핑 VFX 스케일 배율")]
|
||||
[Min(0.01f)]
|
||||
public float loopingVfxScaleMultiplier = 1f;
|
||||
|
||||
[Tooltip("루핑 VFX를 대상의 자식으로 붙일지 여부")]
|
||||
public bool parentLoopingVfxToTarget = true;
|
||||
|
||||
[Header("스탯 수정자")]
|
||||
[Tooltip("스탯에 적용할 수정자 목록")]
|
||||
public List<AbnormalityStatModifier> statModifiers = new List<AbnormalityStatModifier>();
|
||||
@@ -110,6 +127,11 @@ namespace Colosseum.Abnormalities
|
||||
/// </summary>
|
||||
public bool HasControlEffect => controlType != ControlType.None;
|
||||
|
||||
/// <summary>
|
||||
/// 유지형 루핑 VFX가 있는지 확인
|
||||
/// </summary>
|
||||
public bool HasLoopingVfx => loopingVfxPrefab != null;
|
||||
|
||||
/// <summary>
|
||||
/// 받는 피해 배율 변경 여부
|
||||
/// </summary>
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace Colosseum.Abnormalities
|
||||
|
||||
// 활성화된 이상 상태 목록
|
||||
private readonly List<ActiveAbnormality> activeAbnormalities = new List<ActiveAbnormality>();
|
||||
private readonly Dictionary<int, GameObject> abnormalityVisualInstances = new Dictionary<int, GameObject>();
|
||||
|
||||
// 제어 효과 상태
|
||||
private int stunCount;
|
||||
@@ -150,6 +151,8 @@ namespace Colosseum.Abnormalities
|
||||
{
|
||||
SyncControlEffects();
|
||||
}
|
||||
|
||||
RefreshAbnormalityVisuals();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
@@ -165,6 +168,8 @@ namespace Colosseum.Abnormalities
|
||||
{
|
||||
networkController.OnDeathStateChanged -= HandleDeathStateChanged;
|
||||
}
|
||||
|
||||
ClearAllAbnormalityVisuals();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
@@ -255,6 +260,7 @@ namespace Colosseum.Abnormalities
|
||||
ApplyControlEffect(data);
|
||||
RecalculateIncomingDamageMultiplier();
|
||||
SyncAbnormalityAdd(newAbnormality, source);
|
||||
RefreshAbnormalityVisuals();
|
||||
|
||||
OnAbnormalityAdded?.Invoke(newAbnormality);
|
||||
OnAbnormalitiesChanged?.Invoke();
|
||||
@@ -301,6 +307,7 @@ namespace Colosseum.Abnormalities
|
||||
RecalculateIncomingDamageMultiplier();
|
||||
SyncAbnormalityRemove(abnormality);
|
||||
activeAbnormalities.Remove(abnormality);
|
||||
RefreshAbnormalityVisuals();
|
||||
|
||||
OnAbnormalityRemoved?.Invoke(abnormality);
|
||||
OnAbnormalitiesChanged?.Invoke();
|
||||
@@ -593,6 +600,7 @@ namespace Colosseum.Abnormalities
|
||||
|
||||
private void OnSyncedAbnormalitiesChanged(NetworkListEvent<AbnormalitySyncData> changeEvent)
|
||||
{
|
||||
RefreshAbnormalityVisuals();
|
||||
OnAbnormalitiesChanged?.Invoke();
|
||||
}
|
||||
|
||||
@@ -651,6 +659,111 @@ namespace Colosseum.Abnormalities
|
||||
Debug.Log($"[Abnormality] Cleared all abnormalities on death: {gameObject.name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 활성 이상상태 기준으로 루핑 VFX를 동기화합니다.
|
||||
/// </summary>
|
||||
private void RefreshAbnormalityVisuals()
|
||||
{
|
||||
HashSet<int> desiredAbnormalityIds = new HashSet<int>();
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
for (int i = 0; i < activeAbnormalities.Count; i++)
|
||||
{
|
||||
ActiveAbnormality abnormality = activeAbnormalities[i];
|
||||
if (abnormality?.Data == null || !abnormality.Data.HasLoopingVfx)
|
||||
continue;
|
||||
|
||||
int abnormalityId = abnormality.Data.GetInstanceID();
|
||||
desiredAbnormalityIds.Add(abnormalityId);
|
||||
|
||||
if (!abnormalityVisualInstances.ContainsKey(abnormalityId) || abnormalityVisualInstances[abnormalityId] == null)
|
||||
{
|
||||
abnormalityVisualInstances[abnormalityId] = CreateLoopingVfxInstance(abnormality.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < syncedAbnormalities.Count; i++)
|
||||
{
|
||||
AbnormalitySyncData syncData = syncedAbnormalities[i];
|
||||
AbnormalityData data = FindAbnormalityDataById(syncData.AbnormalityId);
|
||||
if (data == null || !data.HasLoopingVfx)
|
||||
continue;
|
||||
|
||||
desiredAbnormalityIds.Add(syncData.AbnormalityId);
|
||||
|
||||
if (!abnormalityVisualInstances.ContainsKey(syncData.AbnormalityId) || abnormalityVisualInstances[syncData.AbnormalityId] == null)
|
||||
{
|
||||
abnormalityVisualInstances[syncData.AbnormalityId] = CreateLoopingVfxInstance(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<int> removeIds = new List<int>();
|
||||
foreach (KeyValuePair<int, GameObject> pair in abnormalityVisualInstances)
|
||||
{
|
||||
if (desiredAbnormalityIds.Contains(pair.Key))
|
||||
continue;
|
||||
|
||||
if (pair.Value != null)
|
||||
Destroy(pair.Value);
|
||||
|
||||
removeIds.Add(pair.Key);
|
||||
}
|
||||
|
||||
for (int i = 0; i < removeIds.Count; i++)
|
||||
{
|
||||
abnormalityVisualInstances.Remove(removeIds[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 루핑 VFX 인스턴스를 생성합니다.
|
||||
/// </summary>
|
||||
private GameObject CreateLoopingVfxInstance(AbnormalityData data)
|
||||
{
|
||||
if (data == null || data.loopingVfxPrefab == null)
|
||||
return null;
|
||||
|
||||
Transform parent = data.parentLoopingVfxToTarget ? transform : null;
|
||||
GameObject instance = Instantiate(data.loopingVfxPrefab, transform.position + data.loopingVfxOffset, Quaternion.identity, parent);
|
||||
instance.transform.localScale *= data.loopingVfxScaleMultiplier;
|
||||
|
||||
if (data.parentLoopingVfxToTarget)
|
||||
instance.transform.localPosition = data.loopingVfxOffset;
|
||||
else
|
||||
instance.transform.position = transform.position + data.loopingVfxOffset;
|
||||
|
||||
ParticleSystem[] particleSystems = instance.GetComponentsInChildren<ParticleSystem>(true);
|
||||
for (int i = 0; i < particleSystems.Length; i++)
|
||||
{
|
||||
ParticleSystem particleSystem = particleSystems[i];
|
||||
var main = particleSystem.main;
|
||||
main.loop = true;
|
||||
main.stopAction = ParticleSystemStopAction.None;
|
||||
particleSystem.Clear(true);
|
||||
particleSystem.Play(true);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 관리 중인 루핑 VFX를 모두 제거합니다.
|
||||
/// </summary>
|
||||
private void ClearAllAbnormalityVisuals()
|
||||
{
|
||||
foreach (KeyValuePair<int, GameObject> pair in abnormalityVisualInstances)
|
||||
{
|
||||
if (pair.Value != null)
|
||||
Destroy(pair.Value);
|
||||
}
|
||||
|
||||
abnormalityVisualInstances.Clear();
|
||||
}
|
||||
|
||||
private void TryCancelCurrentSkill(SkillCancelReason reason, string sourceName)
|
||||
{
|
||||
if (!IsServer || skillController == null || !skillController.IsPlayingAnimation)
|
||||
@@ -686,6 +799,9 @@ namespace Colosseum.Abnormalities
|
||||
/// </summary>
|
||||
public bool HasAbnormality(AbnormalityData data)
|
||||
{
|
||||
if (data == null)
|
||||
return false;
|
||||
|
||||
return activeAbnormalities.Exists(a => a.Data.abnormalityName == data.abnormalityName);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user