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 int stunCount; private int silenceCount; private int invincibleCount; private int hitReactionImmuneCount; 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 syncedHitReactionImmuneCount = 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 IsHitReactionImmune => GetCurrentHitReactionImmuneCount() > 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; syncedHitReactionImmuneCount.OnValueChanged += HandleSyncedHitReactionImmuneChanged; syncedSlowMultiplier.OnValueChanged += HandleSyncedSlowChanged; if (networkController != null) { networkController.OnDeathStateChanged += HandleDeathStateChanged; } if (IsServer) { SyncControlEffects(); } } public override void OnNetworkDespawn() { syncedAbnormalities.OnListChanged -= OnSyncedAbnormalitiesChanged; syncedStunCount.OnValueChanged -= HandleSyncedStunChanged; syncedSilenceCount.OnValueChanged -= HandleSyncedSilenceChanged; syncedInvincibleCount.OnValueChanged -= HandleSyncedInvincibleChanged; syncedHitReactionImmuneCount.OnValueChanged -= HandleSyncedHitReactionImmuneChanged; syncedSlowMultiplier.OnValueChanged -= HandleSyncedSlowChanged; if (networkController != null) { networkController.OnDeathStateChanged -= HandleDeathStateChanged; } } 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); 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); 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.ignoreHitReaction) { hitReactionImmuneCount++; } 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.ignoreHitReaction) { hitReactionImmuneCount = Mathf.Max(0, hitReactionImmuneCount - 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 GetCurrentHitReactionImmuneCount() => IsServer ? hitReactionImmuneCount : syncedHitReactionImmuneCount.Value; private float GetCurrentSlowMultiplier() => IsServer ? slowMultiplier : syncedSlowMultiplier.Value; private void SyncControlEffects() { if (!IsServer) return; syncedStunCount.Value = stunCount; syncedSilenceCount.Value = silenceCount; syncedInvincibleCount.Value = invincibleCount; syncedHitReactionImmuneCount.Value = hitReactionImmuneCount; 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) { 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 HandleSyncedHitReactionImmuneChanged(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}"); } 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) { return activeAbnormalities.Exists(a => a.Data.abnormalityName == data.abnormalityName); } } }