using System.Collections.Generic; using UnityEngine; using Colosseum.Abnormalities; namespace Colosseum.Combat { /// /// 개별 보호막 인스턴스입니다. /// 같은 종류의 보호막은 하나로 합쳐지고, 다른 종류의 보호막은 독립적으로 유지됩니다. /// [System.Serializable] public sealed class ActiveShield { [SerializeField] private AbnormalityData shieldType; [SerializeField] private float remainingAmount; [SerializeField] private float remainingDuration; [SerializeField] private bool isPermanent; [SerializeField] private GameObject source; public AbnormalityData ShieldType => shieldType; public float RemainingAmount => remainingAmount; public float RemainingDuration => isPermanent ? 0f : remainingDuration; public bool IsPermanent => isPermanent; public bool IsExpired => !isPermanent && remainingDuration <= 0f; public bool IsDepleted => remainingAmount <= 0f; public GameObject Source => source; public ActiveShield(AbnormalityData shieldType, float amount, float duration, GameObject source) { this.shieldType = shieldType; remainingAmount = Mathf.Max(0f, amount); isPermanent = duration <= 0f; remainingDuration = isPermanent ? 0f : duration; this.source = source; } /// /// 같은 종류 보호막이 다시 적용되었을 때 양을 더하고 지속시간을 갱신합니다. /// public float Add(float amount, float duration, GameObject source) { float appliedAmount = Mathf.Max(0f, amount); remainingAmount += appliedAmount; if (duration <= 0f) { isPermanent = true; remainingDuration = 0f; } else if (!isPermanent) { remainingDuration = duration; } if (source != null) { this.source = source; } return appliedAmount; } /// /// 지속시간을 감소시킵니다. /// public bool Tick(float deltaTime) { if (isPermanent || deltaTime <= 0f) return false; remainingDuration = Mathf.Max(0f, remainingDuration - deltaTime); return IsExpired; } /// /// 들어오는 피해를 흡수합니다. /// public float Consume(float incomingDamage) { if (incomingDamage <= 0f || remainingAmount <= 0f) return 0f; float absorbed = Mathf.Min(remainingAmount, incomingDamage); remainingAmount = Mathf.Max(0f, remainingAmount - absorbed); return absorbed; } } /// /// 대상이 가진 보호막 인스턴스를 관리합니다. /// [System.Serializable] public sealed class ShieldCollection { private readonly List activeShields = new List(); public IReadOnlyList ActiveShields => activeShields; public float TotalAmount { get { float total = 0f; for (int i = 0; i < activeShields.Count; i++) { total += activeShields[i].RemainingAmount; } return total; } } /// /// 보호막을 적용합니다. 같은 종류는 자기 자신만 합산 및 갱신합니다. /// public float ApplyShield(AbnormalityData shieldType, float amount, float duration, GameObject source) { if (amount <= 0f) return 0f; ActiveShield existingShield = FindShield(shieldType); if (existingShield != null) { return existingShield.Add(amount, duration, source); } activeShields.Add(new ActiveShield(shieldType, amount, duration, source)); return amount; } /// /// 보호막이 적용된 순서대로 피해를 흡수하고 남은 피해를 반환합니다. /// public float ConsumeDamage(float incomingDamage) { if (incomingDamage <= 0f || activeShields.Count == 0) return incomingDamage; float remainingDamage = incomingDamage; for (int i = 0; i < activeShields.Count && remainingDamage > 0f; i++) { remainingDamage -= activeShields[i].Consume(remainingDamage); } CleanupInactiveShields(); return Mathf.Max(0f, remainingDamage); } /// /// 지속시간 경과를 처리합니다. /// public bool Tick(float deltaTime) { bool changed = false; for (int i = activeShields.Count - 1; i >= 0; i--) { ActiveShield shield = activeShields[i]; bool expired = shield.Tick(deltaTime); if (!expired && !shield.IsDepleted) continue; activeShields.RemoveAt(i); changed = true; } return changed; } /// /// 모든 보호막을 제거합니다. /// public void Clear() { activeShields.Clear(); } private ActiveShield FindShield(AbnormalityData shieldType) { for (int i = 0; i < activeShields.Count; i++) { if (activeShields[i].ShieldType == shieldType) return activeShields[i]; } return null; } private void CleanupInactiveShields() { for (int i = activeShields.Count - 1; i >= 0; i--) { if (!activeShields[i].IsExpired && !activeShields[i].IsDepleted) continue; activeShields.RemoveAt(i); } } } /// /// 보호막 수치와 보호막 이상상태를 동기화하는 유틸리티입니다. /// public static class ShieldAbnormalityUtility { /// /// 활성 보호막 목록에 맞춰 보호막 이상상태를 적용하거나 제거합니다. /// public static void SyncShieldAbnormalities( AbnormalityManager abnormalityManager, IReadOnlyList activeShields, GameObject defaultSource) { if (abnormalityManager == null) return; HashSet desiredShieldStates = new HashSet(); Dictionary shieldSources = new Dictionary(); if (activeShields != null) { for (int i = 0; i < activeShields.Count; i++) { ActiveShield shield = activeShields[i]; if (shield == null || shield.RemainingAmount <= 0f || shield.ShieldType == null) continue; desiredShieldStates.Add(shield.ShieldType); if (!shieldSources.ContainsKey(shield.ShieldType)) { shieldSources.Add(shield.ShieldType, shield.Source != null ? shield.Source : defaultSource); } } } foreach (AbnormalityData shieldState in desiredShieldStates) { if (abnormalityManager.HasAbnormality(shieldState)) continue; GameObject source = shieldSources.TryGetValue(shieldState, out GameObject resolvedSource) ? resolvedSource : defaultSource; abnormalityManager.ApplyAbnormality(shieldState, source); } IReadOnlyList activeAbnormalities = abnormalityManager.ActiveAbnormalities; List removeList = new List(); for (int i = 0; i < activeAbnormalities.Count; i++) { ActiveAbnormality activeAbnormality = activeAbnormalities[i]; if (activeAbnormality?.Data == null || !activeAbnormality.Data.isShieldState) continue; if (desiredShieldStates.Contains(activeAbnormality.Data)) continue; removeList.Add(activeAbnormality.Data); } for (int i = 0; i < removeList.Count; i++) { abnormalityManager.RemoveAbnormality(removeList[i]); } } } }