Attack/ITimeline.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using Sandbox;

namespace MANIFOLD.BHLib {
    /// <summary>
    /// Interface for event timelines. Not meant to be used directly outside of the editor.
    /// </summary>
    public interface ITimeline {
        public IReadOnlyList<AttackEvent> Events { get; }
        public Type EventType { get; }

        public AttackEvent GetEvent(Guid id);
        public void AddEvent(AttackEvent evt);
        public void RemoveEvent(AttackEvent evt);
        public void Sort();
    }
    
    /// <summary>
    /// A timeline with events.
    /// </summary>
    /// <typeparam name="T">The type of event.</typeparam>
    public class Timeline<T> : ITimeline where T : AttackEvent {
        public const string TYPE_FIELD = "__type";
        
        [JsonIgnore]
        public List<T> Events { get; set; } = new List<T>();

        IReadOnlyList<AttackEvent> ITimeline.Events => Events;
        [Hide, JsonIgnore]
        public Type EventType => typeof(T);
        
        [Hide]
        public JsonArray SerializedEvents {
            get {
                JsonArray arr = new JsonArray();
                foreach (var module in Events) {
                    var jsonNode = Json.ToNode(module);
                    jsonNode[TYPE_FIELD] = Json.ToNode(module.GetType(), typeof(Type));
                    arr.Add(jsonNode);
                }
                return arr;
            }
            set {
                Events.Clear();
                foreach (var node in value) {
                    var type = Json.FromNode<Type>(node[TYPE_FIELD]);
                    var deserialized = (T)Json.Deserialize(node.ToString(), type);
                    Events.Add(deserialized);
                }
            }
        }

        public T GetEvent(Guid id) {
            return Events.FirstOrDefault(x => x.ID == id);
        }
        AttackEvent ITimeline.GetEvent(Guid id) => GetEvent(id);
        
        public void AddEvent(AttackEvent evt) {
            if (evt is not T casted) throw new ArgumentException("Invalid event type", nameof(evt));
            Events.Add(casted);
        }

        public void RemoveEvent(AttackEvent evt) {
            if (evt is not T casted) throw new ArgumentException("Invalid event type", nameof(evt));
            Events.Remove(casted);
        }

        public void Sort() {
            Events.Sort(new AttackEventComparer());
        }
    }

    /// <summary>
    /// Allows for easy sampling of a timeline.
    /// </summary>
    /// <typeparam name="T">The type of event.</typeparam>
    public class TimelineSampler<T> where T : AttackEvent {
        private readonly Timeline<T> timeline;

        private float time;
        private int currentIndex;
        private T[] cache;

        public float Time => time;
        public bool Exhausted => currentIndex == timeline.Events.Count;
        
        public TimelineSampler(Timeline<T> timeline) {
            this.timeline = timeline;
            time = 0;
            currentIndex = 0;
            cache = new T[20]; // just a guess
        }

        public IEnumerable<T> Read(float delta) {
            if (Exhausted) return [];
            if (delta < 0) throw new ArgumentOutOfRangeException(nameof(delta));

            time += delta;
            
            int localIndex = 0;
            while (currentIndex < timeline.Events.Count) {
                var evt = timeline.Events[currentIndex];
                if (evt.Time <= time) {
                    cache[localIndex] = evt;
                    localIndex++;
                } else {
                    break;
                }
                currentIndex++;
            }

            if (localIndex == 0) return [];
            return cache[..localIndex];
        }
    }
}