using System; using System.Collections.Generic; namespace ProjectI.Builds { public sealed class AbilityStateMachine { private sealed class Charges { public long Available = 1, Capacity = 1; public double Next, Duration, LastCast = double.NegativeInfinity; public double LastReady; } private readonly ulong owner; private readonly PlayerBuildState build; private readonly Dictionary charges = new Dictionary(); private uint lastSequence; public UltimateChargeState Ultimate { get; } = new UltimateChargeState(); public AbilityStateMachine(ulong owner, PlayerBuildState build) { this.owner = owner; this.build = build ?? throw new ArgumentNullException(nameof(build)); } private Charges State(int id) { if (!charges.TryGetValue(id, out var value)) { value = new Charges { Duration = build.Catalog[id].Cooldown }; charges.Add(id, value); } return value; } private static bool Finite(double value) => !double.IsNaN(value) && !double.IsInfinity(value) && value >= 0; private static void Tick(Charges value, double now) { if (!Finite(now) || value.Available >= value.Capacity || value.Next > now) return; if (value.Duration <= 0) { value.Available = value.Capacity; value.LastReady = now; value.Next = 0; return; } long missing = value.Capacity - value.Available; double due = 1 + Math.Floor((now - value.Next) / value.Duration); long gained = due >= missing ? missing : (long)due; value.Available += gained; value.LastReady = value.Next + (gained - 1) * value.Duration; value.Next = value.Available < value.Capacity ? value.Next + gained * value.Duration : 0; } public bool TryActivate(ulong sender, uint sequence, int slot, bool canAct, double now, out AbilityTuning ability) { ability = null; if (sender != owner || sequence == 0 || sequence <= lastSequence) return false; lastSequence = sequence; if (slot < 0 || slot > EquippedAbilitySlot.Melee || !canAct || !Finite(now)) return false; int id = EquippedId(slot); if (!build.Catalog.TryGetValue(id, out var tuning)) return false; if (slot == EquippedAbilitySlot.Ultimate) { if (tuning.Slot != AbilitySlot.Ultimate || !Ultimate.TryConsume(now, tuning.Duration)) return false; ability = tuning; return true; } var value = State(id); Tick(value, now); double interval = tuning.Effect == AbilityEffect.Dash || tuning.RecastPolicy == AbilityRecastPolicy.WaitForMovement ? Math.Max(tuning.MinimumCastInterval, tuning.Duration) : tuning.MinimumCastInterval; if (value.Available <= 0 || now < value.LastCast + interval || double.IsInfinity(now + value.Duration)) return false; if (value.Available == value.Capacity) value.Next = now + value.Duration; value.Available--; value.LastCast = now; ability = tuning; return true; } public void ConfigureCharges(int abilityId, long capacity, double rechargeRate, double now) { if (!Finite(now) || !Finite(rechargeRate) || rechargeRate <= 0 || !build.Catalog.TryGetValue(abilityId, out var tuning)) return; var value = State(abilityId); Tick(value, now); capacity = tuning.Slot == AbilitySlot.Active ? Math.Max(1, capacity) : 1; double duration = tuning.Cooldown / rechargeRate; if (double.IsInfinity(duration)) return; if (value.Available < value.Capacity && value.Duration > 0) value.Next = now + Math.Max(0, (value.Next - now) / value.Duration) * duration; if (capacity > value.Capacity) value.Available += capacity - value.Capacity; value.Capacity = capacity; value.Available = Math.Min(capacity, value.Available); value.Duration = duration; } public long ChargesAvailable(int slot, double now) { var value = State(EquippedId(slot)); Tick(value, now); return value.Available; } public long ChargeCapacity(int slot) => State(EquippedId(slot)).Capacity; public double NextChargeAt(int slot) => State(EquippedId(slot)).Next; public double RechargeDuration(int slot) => State(EquippedId(slot)).Duration; public double ReadyAt(int slot) { if (slot < 0 || slot > EquippedAbilitySlot.Melee) throw new ArgumentOutOfRangeException(nameof(slot)); if (slot == EquippedAbilitySlot.Ultimate) return 0; var value = State(EquippedId(slot)); return value.Available > 0 ? value.LastReady : value.Next; } public void ResetCooldowns() { foreach (var value in charges.Values) { value.Available = value.Capacity; value.Next = value.LastReady = 0; value.LastCast = double.NegativeInfinity; } } public void ReduceActiveCooldowns(double now, double seconds) { if (!Finite(now) || !Finite(seconds) || seconds <= 0) return; for (int slot = 0; slot < EquippedAbilitySlot.NormalCount; slot++) Reduce(State(EquippedId(slot)), now, seconds); } public void ReduceActiveCooldownFractions(double now, double fraction) { if (!Finite(now) || !Finite(fraction) || fraction <= 0) return; for (int slot = 0; slot < EquippedAbilitySlot.NormalCount; slot++) { int id = EquippedId(slot); Reduce(State(id), now, build.Catalog[id].Cooldown * fraction); } } private static void Reduce(Charges value, double now, double seconds) { Tick(value, now); if (value.Available >= value.Capacity) return; // Shift the whole sequential queue, then analytically finish all work paid by the refund. long before = value.Available; value.Next -= seconds; Tick(value, now); if (value.Available > before) value.LastReady = now; } private int EquippedId(int slot) => build.Loadout.EquippedId(slot); } }