Code/AttackCaster.cs
using System;
using System.Collections.Generic;
using System.Linq;
using MANIFOLD.BHLib.Events;
using Sandbox;
namespace MANIFOLD.BHLib {
/// <summary>
/// Used to play attacks.
/// </summary>
[Category(LibraryData.CATEGORY)]
[Icon("stream")]
public class AttackCaster : Component {
public class Instance {
private readonly AttackCaster caster;
private readonly AttackData attack;
private TimelineSampler<SpawnEvent> spawnSampler;
private TimelineSampler<CasterEvent> casterSampler;
private bool durationPassed;
public AttackData Attack => attack;
public float Time => spawnSampler.Time;
public float NormalizedTime => spawnSampler.Time / attack.Duration;
/// <summary>
/// Called when we pass the duration of this attack.
/// </summary>
public event Action<Instance> OnEnded;
/// <summary>
/// Called when all events have been triggered.
/// </summary>
public event Action<Instance> OnSequenceEnded;
public Instance(AttackCaster caster, AttackData data) {
this.caster = caster;
attack = data;
spawnSampler = new TimelineSampler<SpawnEvent>(attack.SpawnTimeline);
casterSampler = new TimelineSampler<CasterEvent>(attack.CasterTimeline);
}
public bool Update(float deltaTime) {
var spawnEvents = spawnSampler.Read(deltaTime);
foreach (var evt in spawnEvents) {
float timePassed = spawnSampler.Time - evt.Time;
if (evt is SpawnEntity entEvt) {
caster.CreateEntity(entEvt.Data, new Transform(evt.Position, evt.Rotation), timePassed);
} else {
Log.Warning($"No spawn implementation for type: {evt.GetType()}. Skipping...");
}
}
var casterEvents = casterSampler.Read(deltaTime);
foreach (var evt in casterEvents) {
foreach (IEventListener listener in caster.listeners) {
listener.OnCasterEvent(evt);
}
}
var passed = spawnSampler.Time >= attack.Duration;
if (passed && !durationPassed) {
OnEnded?.Invoke(this);
durationPassed = true;
}
// Always use the calculated duration since we want to play all events.
bool sequenceEnded = spawnSampler.Time >= attack.CalculatedDuration;
if (sequenceEnded) {
OnSequenceEnded?.Invoke(this);
}
return sequenceEnded;
}
}
/// <summary>
/// Allows for easy access from editor tools. Also used to infer pooling capacities.
/// </summary>
[Property]
public List<AttackData> Attacks { get; set; } = new List<AttackData>();
/// <summary>
/// If used it will try to get an <see cref="ITarget"/> from it.
/// </summary>
[Property]
public GameObject Target {
get => gameObjTarget;
set {
gameObjTarget = value;
RealTarget = gameObjTarget.GetComponent<ITarget>();
}
}
private List<Instance> instances;
private IEventListener[] listeners;
private GameObject gameObjTarget;
// PREVIEW
private bool inPreview;
private AttackData previewData;
private Dictionary<SpawnEvent, PreviewEntity> previewEntities;
// ACCESSORS
public ITarget RealTarget { get; set; }
public IReadOnlyList<Instance> Instances => instances;
// PREVIEW ACCESSORS
public bool InPreviewMode => inPreview;
public AttackData PreviewedAttack => previewData;
protected override void OnAwake() {
instances = new List<Instance>();
listeners = GetComponents<IEventListener>().ToArray();
}
protected override void OnStart() {
RealTarget = Target.GetComponent<ITarget>();
CreatePools();
}
protected override void OnFixedUpdate() {
for (int i = 0; i < instances.Count; i++) {
bool finished = instances[i].Update(Time.Delta);
if (finished) {
instances.RemoveAt(i);
i--;
}
}
}
// ATTACK API
public void PlayAttack(AttackData data) {
instances.Add(new Instance(this, data));
}
public void StopAttack(Instance instance) {
if (!instances.Contains(instance)) {
throw new ArgumentException("Instance does not belong to this caster.", nameof(instance));
}
instances.Remove(instance);
}
public void StopAllAttacks() {
instances.Clear();
}
// UTILITY
private void CreatePools() {
Dictionary<GameObject, int> cache = new Dictionary<GameObject, int>();
foreach (var attack in Attacks) {
foreach (var renderer in attack.RenderPoolingData) {
if (cache.TryGetValue(renderer.Prefab, out var capacity)) {
int newCapacity = Math.Max(capacity, renderer.UseCount);
cache[renderer.Prefab] = newCapacity;
} else {
cache.Add(renderer.Prefab, renderer.UseCount);
}
}
}
foreach (var pair in cache) {
RendererPool.CreatePool(pair.Key, (pair.Value * 1.5f).CeilToInt());
}
}
private GameObject CreateEntity(EntityData data, Transform localTransform, float? simulate = null) {
GameObject go = Scene.CreateObject();
go.WorldTransform = LocalTransform.ToWorld(localTransform);
foreach (var component in data.Components) {
if (!component.Enabled) continue;
var inst = component.Create(go);
inst.Owner = GameObject;
inst.Target = RealTarget;
if (simulate.HasValue) {
inst.SimulateFrame(simulate.Value);
}
}
return go;
}
// PREVIEW UTILITY
public void StartPreview(AttackData data) {
if (inPreview) return;
previewEntities = new Dictionary<SpawnEvent, PreviewEntity>();
previewData = data;
inPreview = true;
RebuildPreviewEntities();
}
public void StopPreview() {
if (!inPreview) return;
CleanupPreviewEntities();
previewEntities = null;
previewData = null;
inPreview = false;
}
public void ResimulatePreview(float time) {
foreach (var entity in previewEntities.Values) {
entity.Resimulate(time);
}
}
public void RebuildEntity(SpawnEntity evt) {
if (previewEntities.TryGetValue(evt, out PreviewEntity existingEnt)) {
existingEnt.GameObject.DestroyImmediate();
previewEntities.Remove(evt);
}
var result = CreatePreviewEntity(evt);
if (result.IsValid()) previewEntities.Add(evt, result);
}
/// <summary>
/// This will catch any timeline additions/removals.
/// </summary>
/// <param name="full"></param>
public void RebuildPreviewEntities(bool full = false) {
if (full) {
CleanupPreviewEntities();
}
// additions
foreach (var evt in previewData.SpawnTimeline.Events) {
if (evt is not SpawnEntity entSpawn) continue;
if (previewEntities.ContainsKey(evt)) continue;
var result = CreatePreviewEntity(entSpawn);
if (result.IsValid()) previewEntities.Add(evt, result);
}
}
private void CleanupPreviewEntities() {
foreach (var entity in previewEntities.Values) {
entity.GameObject.DestroyImmediate();
}
previewEntities.Clear();
}
private PreviewEntity CreatePreviewEntity(SpawnEntity evt) {
if (evt.Data == null) {
Log.Warning($"Tried to create preview entity with no data! Event: {evt.Name} ({evt.ID})");
return null;
}
var go = CreateEntity(evt.Data, new Transform(evt.Position, evt.Rotation));
var preview = go.AddComponent<PreviewEntity>();
preview.SpawnTime = evt.Time;
preview.Initialize();
return preview;
}
}
}