using System;
using System.Collections.Generic;
using UnityEngine;
using Unity.Netcode;
using Colosseum.Stats;
using Colosseum.Player;
using Colosseum.Skills;
namespace Colosseum.Abnormalities
{
///
/// 캐릭터에 부착되어 이상 상태를 관리하는 컴포넌트
/// 버프/디버프의 적용, 제거, 주기적 효과를 처리합니다.
///
public class AbnormalityManager : NetworkBehaviour
{
[Header("References")]
[Tooltip("CharacterStats 컴포넌트 (없으면 자동 검색)")]
[SerializeField] private CharacterStats characterStats;
[Tooltip("PlayerNetworkController 컴포넌트 (HP/MP 관리용)")]
[SerializeField] private PlayerNetworkController networkController;
[Tooltip("스킬 실행 관리자 (강제 취소 처리용)")]
[SerializeField] private SkillController skillController;
// 활성화된 이상 상태 목록
private readonly List activeAbnormalities = new List();
private readonly Dictionary abnormalityVisualInstances = new Dictionary();
// 제어 효과 상태
private int stunCount;
private int silenceCount;
private int invincibleCount;
private int staggerImmuneCount;
private int knockbackImmuneCount;
private int downImmuneCount;
private float slowMultiplier = 1f;
private float incomingDamageMultiplier = 1f;
// 클라이언트 판정용 제어 효과 동기화 변수
private NetworkVariable syncedStunCount = new NetworkVariable(0);
private NetworkVariable syncedSilenceCount = new NetworkVariable(0);
private NetworkVariable syncedInvincibleCount = new NetworkVariable(0);
private NetworkVariable syncedStaggerImmuneCount = new NetworkVariable(0);
private NetworkVariable syncedKnockbackImmuneCount = new NetworkVariable(0);
private NetworkVariable syncedDownImmuneCount = new NetworkVariable(0);
private NetworkVariable syncedSlowMultiplier = new NetworkVariable(1f);
// 네트워크 동기화용 데이터
private NetworkList syncedAbnormalities;
///
/// 기절 상태 여부
///
public bool IsStunned => GetCurrentStunCount() > 0;
///
/// 침묵 상태 여부
///
public bool IsSilenced => GetCurrentSilenceCount() > 0;
///
/// 무적 상태 여부
///
public bool IsInvincible => GetCurrentInvincibleCount() > 0;
///
/// 경직 면역 상태 여부
///
public bool IsStaggerImmune => GetCurrentStaggerImmuneCount() > 0;
///
/// 넉백 면역 상태 여부
///
public bool IsKnockbackImmune => GetCurrentKnockbackImmuneCount() > 0;
///
/// 다운 면역 상태 여부
///
public bool IsDownImmune => GetCurrentDownImmuneCount() > 0;
///
/// 이동 속도 배율 (1.0 = 기본, 0.5 = 50% 감소)
///
public float MoveSpeedMultiplier => GetCurrentSlowMultiplier();
///
/// 받는 피해 배율 (1.0 = 기본, 1.1 = 10% 증가)
///
public float IncomingDamageMultiplier => incomingDamageMultiplier;
///
/// 행동 가능 여부 (기절이 아닐 때)
///
public bool CanAct => !IsStunned;
///
/// 스킬 사용 가능 여부
///
public bool CanUseSkills => !IsStunned && !IsSilenced;
///
/// 활성화된 이상 상태 목록 (읽기 전용)
///
public IReadOnlyList ActiveAbnormalities => activeAbnormalities;
// 이벤트
public event Action OnAbnormalityAdded;
public event Action OnAbnormalityRemoved;
public event Action OnAbnormalitiesChanged;
///
/// 네트워크 동기화용 이상 상태 데이터 구조체
///
private struct AbnormalitySyncData : INetworkSerializable, IEquatable
{
public int AbnormalityId;
public float RemainingDuration;
public ulong SourceClientId;
public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref AbnormalityId);
serializer.SerializeValue(ref RemainingDuration);
serializer.SerializeValue(ref SourceClientId);
}
public bool Equals(AbnormalitySyncData other)
{
return AbnormalityId == other.AbnormalityId;
}
}
private void Awake()
{
if (characterStats == null)
characterStats = GetComponent();
if (networkController == null)
networkController = GetComponent();
if (skillController == null)
skillController = GetComponent();
syncedAbnormalities = new NetworkList();
}
public override void OnNetworkSpawn()
{
syncedAbnormalities.OnListChanged += OnSyncedAbnormalitiesChanged;
syncedStunCount.OnValueChanged += HandleSyncedStunChanged;
syncedSilenceCount.OnValueChanged += HandleSyncedSilenceChanged;
syncedInvincibleCount.OnValueChanged += HandleSyncedInvincibleChanged;
syncedStaggerImmuneCount.OnValueChanged += HandleSyncedStaggerImmuneChanged;
syncedKnockbackImmuneCount.OnValueChanged += HandleSyncedKnockbackImmuneChanged;
syncedDownImmuneCount.OnValueChanged += HandleSyncedDownImmuneChanged;
syncedSlowMultiplier.OnValueChanged += HandleSyncedSlowChanged;
if (networkController != null)
{
networkController.OnDeathStateChanged += HandleDeathStateChanged;
}
if (IsServer)
{
SyncControlEffects();
}
RefreshAbnormalityVisuals();
}
public override void OnNetworkDespawn()
{
syncedAbnormalities.OnListChanged -= OnSyncedAbnormalitiesChanged;
syncedStunCount.OnValueChanged -= HandleSyncedStunChanged;
syncedSilenceCount.OnValueChanged -= HandleSyncedSilenceChanged;
syncedInvincibleCount.OnValueChanged -= HandleSyncedInvincibleChanged;
syncedStaggerImmuneCount.OnValueChanged -= HandleSyncedStaggerImmuneChanged;
syncedKnockbackImmuneCount.OnValueChanged -= HandleSyncedKnockbackImmuneChanged;
syncedDownImmuneCount.OnValueChanged -= HandleSyncedDownImmuneChanged;
syncedSlowMultiplier.OnValueChanged -= HandleSyncedSlowChanged;
if (networkController != null)
{
networkController.OnDeathStateChanged -= HandleDeathStateChanged;
}
ClearAllAbnormalityVisuals();
}
private void Update()
{
if (!IsServer) return;
UpdateAbnormalities(Time.deltaTime);
}
///
/// 이상 상태 적용
///
/// 적용할 이상 상태 데이터
/// 효과 시전자
public void ApplyAbnormality(AbnormalityData data, GameObject source)
{
if (data == null)
{
Debug.LogWarning("[Abnormality] ApplyAbnormality called with null data");
return;
}
if (networkController != null && networkController.IsDead)
{
Debug.Log($"[Abnormality] Ignored {data.abnormalityName} because {gameObject.name} is dead");
return;
}
if (IsServer)
{
ApplyAbnormalityInternal(data, source);
}
else
{
var sourceNetId = source != null && source.TryGetComponent(out var netObj) ? netObj.NetworkObjectId : 0UL;
ApplyAbnormalityServerRpc(data.GetInstanceID(), sourceNetId);
}
}
[Rpc(SendTo.Server)]
private void ApplyAbnormalityServerRpc(int dataId, ulong sourceNetworkId)
{
var data = FindAbnormalityDataById(dataId);
if (data == null)
{
Debug.LogWarning($"[Abnormality] Could not find data with ID: {dataId}");
return;
}
GameObject source = null;
if (sourceNetworkId != 0UL && NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(sourceNetworkId, out var netObj))
{
source = netObj.gameObject;
}
ApplyAbnormalityInternal(data, source);
}
private void ApplyAbnormalityInternal(AbnormalityData data, GameObject source)
{
var existing = FindExistingAbnormality(data);
if (existing != null)
{
if (existing.Data == data)
{
existing.RefreshDuration();
UpdateSyncedAbnormalityDuration(existing);
Debug.Log($"[Abnormality] Refreshed {data.abnormalityName} on {gameObject.name}");
return;
}
if (data.level > existing.Data.level)
{
RemoveAbnormalityInternal(existing);
}
else
{
Debug.Log($"[Abnormality] Ignored {data.abnormalityName} (level {data.level}) - existing level {existing.Data.level} is higher or equal");
return;
}
}
var newAbnormality = new ActiveAbnormality(data, source);
activeAbnormalities.Add(newAbnormality);
ApplyStatModifiers(newAbnormality);
ApplyControlEffect(data);
RecalculateIncomingDamageMultiplier();
SyncAbnormalityAdd(newAbnormality, source);
RefreshAbnormalityVisuals();
OnAbnormalityAdded?.Invoke(newAbnormality);
OnAbnormalitiesChanged?.Invoke();
Debug.Log($"[Abnormality] Applied {data.abnormalityName} (level {data.level}) to {gameObject.name} for {data.duration}s");
}
///
/// 이상 상태 제거
///
/// 제거할 이상 상태 데이터
public void RemoveAbnormality(AbnormalityData data)
{
if (data == null) return;
if (IsServer)
{
var abnormality = FindExistingAbnormality(data);
if (abnormality != null)
{
RemoveAbnormalityInternal(abnormality);
}
}
else
{
RemoveAbnormalityServerRpc(data.GetInstanceID());
}
}
[Rpc(SendTo.Server)]
private void RemoveAbnormalityServerRpc(int dataId)
{
var abnormality = activeAbnormalities.Find(a => a.Data.GetInstanceID() == dataId);
if (abnormality != null)
{
RemoveAbnormalityInternal(abnormality);
}
}
private void RemoveAbnormalityInternal(ActiveAbnormality abnormality)
{
RemoveStatModifiers(abnormality);
RemoveControlEffect(abnormality.Data);
RecalculateIncomingDamageMultiplier();
SyncAbnormalityRemove(abnormality);
activeAbnormalities.Remove(abnormality);
RefreshAbnormalityVisuals();
OnAbnormalityRemoved?.Invoke(abnormality);
OnAbnormalitiesChanged?.Invoke();
Debug.Log($"[Abnormality] Removed {abnormality.Data.abnormalityName} from {gameObject.name}");
}
///
/// 모든 이상 상태 제거
///
public void RemoveAllAbnormalities()
{
if (!IsServer)
{
RemoveAllAbnormalitiesServerRpc();
return;
}
while (activeAbnormalities.Count > 0)
{
RemoveAbnormalityInternal(activeAbnormalities[0]);
}
}
[Rpc(SendTo.Server)]
private void RemoveAllAbnormalitiesServerRpc()
{
RemoveAllAbnormalities();
}
///
/// 특정 출처의 모든 이상 상태 제거
///
public void RemoveAbnormalitiesFromSource(GameObject source)
{
if (!IsServer)
{
var sourceNetId = source != null && source.TryGetComponent(out var netObj) ? netObj.NetworkObjectId : 0UL;
RemoveAbnormalitiesFromSourceServerRpc(sourceNetId);
return;
}
for (int i = activeAbnormalities.Count - 1; i >= 0; i--)
{
if (activeAbnormalities[i].Source == source)
{
RemoveAbnormalityInternal(activeAbnormalities[i]);
}
}
}
[Rpc(SendTo.Server)]
private void RemoveAbnormalitiesFromSourceServerRpc(ulong sourceNetworkId)
{
for (int i = activeAbnormalities.Count - 1; i >= 0; i--)
{
var abnormality = activeAbnormalities[i];
var sourceNetId = abnormality.Source != null && abnormality.Source.TryGetComponent(out var netObj) ? netObj.NetworkObjectId : 0UL;
if (sourceNetId == sourceNetworkId)
{
RemoveAbnormalityInternal(abnormality);
}
}
}
private void UpdateAbnormalities(float deltaTime)
{
for (int i = activeAbnormalities.Count - 1; i >= 0; i--)
{
var abnormality = activeAbnormalities[i];
if (abnormality.CanTriggerPeriodic())
{
TriggerPeriodicEffect(abnormality);
}
if (abnormality.Tick(deltaTime))
{
RemoveAbnormalityInternal(abnormality);
}
}
}
private void TriggerPeriodicEffect(ActiveAbnormality abnormality)
{
if (networkController == null) return;
float value = abnormality.Data.periodicValue;
if (value > 0)
{
networkController.RestoreHealthRpc(value);
Debug.Log($"[Abnormality] Periodic heal: +{value} HP from {abnormality.Data.abnormalityName}");
}
else if (value < 0)
{
networkController.TakeDamageRpc(-value);
Debug.Log($"[Abnormality] Periodic damage: {-value} HP from {abnormality.Data.abnormalityName}");
}
}
private ActiveAbnormality FindExistingAbnormality(AbnormalityData data)
{
return activeAbnormalities.Find(a => a.Data.abnormalityName == data.abnormalityName);
}
private void ApplyStatModifiers(ActiveAbnormality abnormality)
{
if (characterStats == null) return;
foreach (var entry in abnormality.Data.statModifiers)
{
var stat = characterStats.GetStat(entry.statType);
if (stat != null)
{
var modifier = new StatModifier(entry.value, entry.modType, abnormality);
abnormality.AppliedModifiers.Add(modifier);
stat.AddModifier(modifier);
}
}
}
private void RemoveStatModifiers(ActiveAbnormality abnormality)
{
if (characterStats == null) return;
foreach (StatType statType in Enum.GetValues(typeof(StatType)))
{
var stat = characterStats.GetStat(statType);
stat?.RemoveAllModifiersFromSource(abnormality);
}
abnormality.AppliedModifiers.Clear();
}
private void ApplyControlEffect(AbnormalityData data)
{
bool enteredStun = false;
if (data.ignoreStagger)
staggerImmuneCount++;
if (data.ignoreKnockback)
knockbackImmuneCount++;
if (data.ignoreDown)
downImmuneCount++;
switch (data.controlType)
{
case ControlType.Stun:
enteredStun = stunCount == 0;
stunCount++;
break;
case ControlType.Silence:
silenceCount++;
break;
case ControlType.Slow:
slowMultiplier = Mathf.Min(slowMultiplier, data.slowMultiplier);
break;
case ControlType.Invincible:
invincibleCount++;
break;
}
SyncControlEffects();
if (enteredStun)
{
TryCancelCurrentSkill(SkillCancelReason.Stun, data.abnormalityName);
}
}
private void RemoveControlEffect(AbnormalityData data)
{
if (data.ignoreStagger)
staggerImmuneCount = Mathf.Max(0, staggerImmuneCount - 1);
if (data.ignoreKnockback)
knockbackImmuneCount = Mathf.Max(0, knockbackImmuneCount - 1);
if (data.ignoreDown)
downImmuneCount = Mathf.Max(0, downImmuneCount - 1);
switch (data.controlType)
{
case ControlType.Stun:
stunCount = Mathf.Max(0, stunCount - 1);
break;
case ControlType.Silence:
silenceCount = Mathf.Max(0, silenceCount - 1);
break;
case ControlType.Slow:
RecalculateSlowMultiplier();
break;
case ControlType.Invincible:
invincibleCount = Mathf.Max(0, invincibleCount - 1);
break;
}
SyncControlEffects();
}
private void RecalculateSlowMultiplier()
{
slowMultiplier = 1f;
foreach (var abnormality in activeAbnormalities)
{
if (abnormality.Data.controlType == ControlType.Slow)
{
slowMultiplier = Mathf.Min(slowMultiplier, abnormality.Data.slowMultiplier);
}
}
}
private void RecalculateIncomingDamageMultiplier()
{
incomingDamageMultiplier = 1f;
for (int i = 0; i < activeAbnormalities.Count; i++)
{
AbnormalityData data = activeAbnormalities[i].Data;
if (data == null || !data.HasIncomingDamageModifier)
continue;
incomingDamageMultiplier *= Mathf.Max(0f, data.incomingDamageMultiplier);
}
}
private int GetCurrentStunCount() => IsServer ? stunCount : syncedStunCount.Value;
private int GetCurrentSilenceCount() => IsServer ? silenceCount : syncedSilenceCount.Value;
private int GetCurrentInvincibleCount() => IsServer ? invincibleCount : syncedInvincibleCount.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 void SyncControlEffects()
{
if (!IsServer)
return;
syncedStunCount.Value = stunCount;
syncedSilenceCount.Value = silenceCount;
syncedInvincibleCount.Value = invincibleCount;
syncedStaggerImmuneCount.Value = staggerImmuneCount;
syncedKnockbackImmuneCount.Value = knockbackImmuneCount;
syncedDownImmuneCount.Value = downImmuneCount;
syncedSlowMultiplier.Value = slowMultiplier;
}
private void SyncAbnormalityAdd(ActiveAbnormality abnormality, GameObject source)
{
var sourceClientId = source != null && source.TryGetComponent(out var netObj) ? netObj.OwnerClientId : 0UL;
var syncData = new AbnormalitySyncData
{
AbnormalityId = abnormality.Data.GetInstanceID(),
RemainingDuration = abnormality.RemainingDuration,
SourceClientId = sourceClientId
};
syncedAbnormalities.Add(syncData);
}
private void UpdateSyncedAbnormalityDuration(ActiveAbnormality abnormality)
{
for (int i = 0; i < syncedAbnormalities.Count; i++)
{
if (syncedAbnormalities[i].AbnormalityId == abnormality.Data.GetInstanceID())
{
var syncData = syncedAbnormalities[i];
syncData.RemainingDuration = abnormality.RemainingDuration;
syncedAbnormalities[i] = syncData;
break;
}
}
}
private void SyncAbnormalityRemove(ActiveAbnormality abnormality)
{
for (int i = 0; i < syncedAbnormalities.Count; i++)
{
if (syncedAbnormalities[i].AbnormalityId == abnormality.Data.GetInstanceID())
{
syncedAbnormalities.RemoveAt(i);
break;
}
}
}
private void OnSyncedAbnormalitiesChanged(NetworkListEvent changeEvent)
{
RefreshAbnormalityVisuals();
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedStunChanged(int oldValue, int newValue)
{
if (oldValue == newValue)
return;
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedSilenceChanged(int oldValue, int newValue)
{
if (oldValue == newValue)
return;
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedSlowChanged(float oldValue, float newValue)
{
if (Mathf.Approximately(oldValue, newValue))
return;
OnAbnormalitiesChanged?.Invoke();
}
private void HandleSyncedInvincibleChanged(int oldValue, int newValue)
{
if (oldValue == newValue)
return;
OnAbnormalitiesChanged?.Invoke();
}
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)
return;
OnAbnormalitiesChanged?.Invoke();
}
///
/// 사망 시 활성 이상 상태를 모두 제거합니다.
///
private void HandleDeathStateChanged(bool dead)
{
if (!dead || !IsServer)
return;
if (activeAbnormalities.Count == 0)
return;
RemoveAllAbnormalities();
Debug.Log($"[Abnormality] Cleared all abnormalities on death: {gameObject.name}");
}
///
/// 현재 활성 이상상태 기준으로 루핑 VFX를 동기화합니다.
///
private void RefreshAbnormalityVisuals()
{
HashSet desiredAbnormalityIds = new HashSet();
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 removeIds = new List();
foreach (KeyValuePair 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]);
}
}
///
/// 루핑 VFX 인스턴스를 생성합니다.
///
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(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;
}
///
/// 현재 관리 중인 루핑 VFX를 모두 제거합니다.
///
private void ClearAllAbnormalityVisuals()
{
foreach (KeyValuePair 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)
return;
if (skillController.CancelSkill(reason))
{
Debug.Log($"[Abnormality] Cancelled skill because '{sourceName}' applied to {gameObject.name}");
}
}
private AbnormalityData FindAbnormalityDataById(int instanceId)
{
var allData = Resources.FindObjectsOfTypeAll();
foreach (var data in allData)
{
if (data.GetInstanceID() == instanceId)
return data;
}
return null;
}
///
/// 특정 이름의 이상 상태가 활성화되어 있는지 확인
///
public bool HasAbnormality(string name)
{
return activeAbnormalities.Exists(a => a.Data.abnormalityName == name);
}
///
/// 특정 데이터의 이상 상태가 활성화되어 있는지 확인
///
public bool HasAbnormality(AbnormalityData data)
{
if (data == null)
return false;
return activeAbnormalities.Exists(a => a.Data.abnormalityName == data.abnormalityName);
}
}
}