using System; using System.Collections.Generic; namespace ProjectI.Combat { // Player-only pools: enemy health continues to use HealthState directly. public sealed class PlayerDefenseState { public const double RechargeDelay = 5; private readonly HealthState health; private readonly SortedDictionary sources = new SortedDictionary(); private readonly List sourceKeys = new List(8); private double lastTick; public float Current => health.Current; public float Maximum => health.Maximum; public bool IsDowned => health.IsDowned; public float Shield { get; private set; } public float MaximumShield { get; private set; } public float OvershieldCap { get; private set; } public double RechargeStartsAt { get; private set; } public float Overshield { get { float total = 0; foreach (var source in sources.Values) total += source.Amount; return total; } } // The caller owns and reuses this destination; gameplay has no authored source-count limit. public void CopyActiveOvershieldSources(List destination) { if(destination==null)throw new ArgumentNullException(nameof(destination)); destination.Clear(); foreach(var source in sources) if(source.Value.Amount>0)destination.Add(new ShieldSourceSnapshot(source.Key,source.Value.Amount,source.Value.Until)); } public PlayerDefenseState(float maximumHealth = 50, float maximumShield = 50, float overshieldCap = 50) { health = new HealthState(maximumHealth); MaximumShield = Finite(maximumShield) ? Math.Max(0, maximumShield) : 0; OvershieldCap = Finite(overshieldCap) ? Math.Max(0, overshieldCap) : 0; Shield = MaximumShield; } public void Tick(double now, bool allowRecharge = true) { if (!ValidTime(now)) return; Expire(now); if (allowRecharge && !IsDowned && now > RechargeStartsAt) { double seconds = Math.Max(0, now - Math.Max(lastTick, RechargeStartsAt)); Shield = (float)Math.Min(MaximumShield, Shield + seconds * MaximumShield * 0.2); } lastTick = now; } public float ApplyDamage(float amount, double now, double rechargeDelayMultiplier=1) { if (IsDowned || !Finite(amount) || amount <= 0 || !ValidTime(now)) return 0; Tick(now); float overflowDamage = ConsumeOvershield(amount); float shieldDamage = Math.Min(Shield, amount - overflowDamage); Shield -= shieldDamage; float healthDamage = health.ApplyDamage(amount - overflowDamage - shieldDamage); float applied = overflowDamage + shieldDamage + healthDamage; if (applied > 0) RechargeStartsAt = now + RechargeDelay/(Finite(rechargeDelayMultiplier)&&rechargeDelayMultiplier>=1?rechargeDelayMultiplier:1); if (IsDowned) ClearShields(); return applied; } public ShieldGrantResult GrantShield(float amount, double now, bool allowOvershield = false, EffectSourceKey sourceId = default, double overshieldDuration = 0) { if (IsDowned || !Finite(amount) || amount <= 0 || !ValidTime(now) || !Finite(overshieldDuration) || overshieldDuration < 0 || (overshieldDuration > 0 && !Finite(now + overshieldDuration))) return default; Tick(now); float restored = Math.Min(MaximumShield - Shield, amount); Shield += restored; float excess = amount - restored; if (!allowOvershield || excess <= 0) return new ShieldGrantResult(restored, 0); sources.TryGetValue(sourceId, out var previous); float available = Math.Max(0, OvershieldCap - (Overshield - previous.Amount)); // Recasts replenish to the authored amount and refresh expiry, rather than stacking. float grantedPool = Math.Min(available, Math.Max(previous.Amount, excess)); float granted = Math.Max(0, grantedPool - previous.Amount); if (grantedPool > 0) sources[sourceId] = (grantedPool, overshieldDuration > 0 ? now + overshieldDuration : double.PositiveInfinity); return new ShieldGrantResult(restored, granted); } public float Heal(float amount) => health.Heal(amount); public void RemoveOvershieldSource(EffectSourceKey source)=>sources.Remove(source); public bool TryRevive(float fraction, double now) { if (!ValidTime(now) || !health.TryRevive(fraction)) return false; ClearShields(); lastTick = now; RechargeStartsAt = now + RechargeDelay; return true; } public void Reset(float maximumHealth, double now) { if (!ValidTime(now) || !Finite(maximumHealth) || maximumHealth <= 0) return; health.Reset(maximumHealth); sources.Clear(); Shield = MaximumShield; RechargeStartsAt = now; lastTick = now; } public void ChangeMaximum(float value) { if (!Finite(value) || value <= 0) return; health.ChangeMaximum(value); if (IsDowned) ClearShields(); } public void ChangeMaximumPreservingMissing(float value) { if (!Finite(value) || value <= 0) return; health.ChangeMaximumPreservingMissing(value); if (IsDowned) ClearShields(); } public void ChangeShieldMaximum(float value, float overshieldCap) { if (!Finite(value) || value < 0 || !Finite(overshieldCap) || overshieldCap < 0) return; MaximumShield = value; OvershieldCap = overshieldCap; Shield = Math.Min(Shield, MaximumShield); ConsumeOvershield(Math.Max(0, Overshield - OvershieldCap)); } public void ChangeNormalShieldMaximumPreservingMissing(float value) { if(!Finite(value)||value<0)return; float missing=Math.Max(0,MaximumShield-Shield); ChangeShieldMaximum(value,OvershieldCap); Shield=IsDowned?0:Math.Max(0,value-missing); } private void ClearShields() { Shield = 0; sources.Clear(); } private bool ValidTime(double now) => Finite(now) && now >= lastTick; private static bool Finite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); private void Expire(double now) { sourceKeys.Clear(); foreach (var pair in sources) if (now >= pair.Value.Until) sourceKeys.Add(pair.Key); foreach (var key in sourceKeys) sources.Remove(key); } private float ConsumeOvershield(float amount) { float consumed = 0; while (amount > 0 && sources.Count > 0) { EffectSourceKey earliest = default; double until = double.PositiveInfinity; bool found = false; foreach (var pair in sources) { if (!found || pair.Value.Until < until) { earliest = pair.Key; until = pair.Value.Until; found = true; } } var source = sources[earliest]; float taken = Math.Min(amount, source.Amount); amount -= taken; consumed += taken; if (taken >= source.Amount) sources.Remove(earliest); else sources[earliest] = (source.Amount - taken, source.Until); } return consumed; } } }