Editor/Attack/AttackEditor.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using MANIFOLD.BHLib;
using MANIFOLD.BHLib.Components;
using MANIFOLD.BHLib.Events;
using Sandbox;
using Sandbox.UI;
using Application = Editor.Application;
using Button = Editor.Button;
using Checkbox = Editor.Checkbox;
using ControlSheet = Editor.ControlSheet;
using Label = Editor.Label;
using Renderer = MANIFOLD.BHLib.Components.Renderer;
namespace MANIFOLD.BHLib.Editor {
[Dock("Editor", "Attack Editor", "sports_martial_arts")]
public class AttackEditor : Widget, EditorEvent.ISceneView {
public class TimelineInfo {
public string name;
public int index;
public ITimeline timeline;
}
// UI
private ControlBar controlBar;
private TimelinePanel timelinePanel;
private InspectPanel inspectPanel;
// ATTACK
private GameObject gameObject;
private AttackCaster caster;
private AttackData selectedAttack;
private TimelineInfo[] availableTimelines;
private ITimeline selectedTimeline;
private int? selectedTimelineIndex;
private AttackEvent selectedEvent;
private Guid? selectedEventId;
private bool isPlaying;
private float currentTime;
private bool autosave = false;
public AttackCaster Caster => caster;
public AttackData SelectedAttack {
get => selectedAttack;
set {
bool reloadPreview = value != selectedAttack && caster.InPreviewMode;
if (reloadPreview) {
caster.StopPreview();
}
selectedAttack = value;
OnAttackChanged();
if (reloadPreview) {
caster.StartPreview(selectedAttack);
}
}
}
public IReadOnlyList<TimelineInfo> AvailableTimelines => availableTimelines;
public ITimeline SelectedTimeline {
get => selectedTimeline;
set {
if (selectedTimeline == value) return;
try {
selectedTimelineIndex = selectedTimeline == null ? null : availableTimelines.First(x => x.timeline == value).index;
selectedTimeline = value;
} catch (InvalidOperationException inEx) {
// This happens when the timeline doesnt belong in the list. Ignore
// Quick fix to some weird issue with the timeline combo box being triggered when its changing
}
SelectedEvent = null;
timelinePanel.OnTimelineSelected();
}
}
public AttackEvent SelectedEvent {
get => selectedEvent;
set {
selectedEvent = value;
selectedEventId = value?.ID;
inspectPanel.RebuildSheet();
}
}
public bool IsPlaying {
get => isPlaying;
set {
isPlaying = value;
}
}
public float CurrentTime {
get => currentTime;
set {
currentTime = value;
if (InPreview) {
caster.ResimulatePreview(currentTime);
}
timelinePanel.OnTimeChanged();
}
}
public bool InPreview {
get => caster.InPreviewMode;
set {
if (value == caster.InPreviewMode) return;
if (value) caster.StartPreview(selectedAttack);
else caster.StopPreview();
}
}
public bool Autosave {
get => autosave;
set => autosave = value;
}
public AttackEditor(Widget parent) : base(parent) {
Layout = Layout.Column();
Layout.Margin = 8;
Layout.Spacing = 8;
EditorUtility.OnInspect += OnObjectInspect;
RebuildUI();
}
public override void OnDestroyed() {
EditorUtility.OnInspect -= OnObjectInspect;
}
public void DrawGizmos(Scene scene) {
if (caster == null) return;
if (selectedEvent is not IDrawGizmos casted) return;
using (Gizmo.Scope("AttackEditor", caster.WorldTransform)) {
casted.DrawGizmos();
}
}
[EditorEvent.Frame]
private void OnFrame() {
if (caster != null && !caster.IsValid()) {
ScanSelectedObject();
}
if (IsPlaying) {
CurrentTime += RealTime.Delta;
}
}
private void OnObjectInspect(EditorUtility.OnInspectArgs args) {
if (caster.IsValid() && caster.InPreviewMode) {
caster.StopPreview();
}
if (args.Object == null) return;
object obj;
if (args.Object is Array arr) {
obj = arr.Length > 0 ? arr.GetValue(0) : null;
} else {
obj = args.Object;
}
gameObject = obj as GameObject;
ScanSelectedObject();
}
public void ScanSelectedObject() {
caster = gameObject?.GetComponent<AttackCaster>();
RebuildUI();
}
// UI
private void RebuildUI() {
if (caster.IsValid() && caster.Attacks.Count > 0) {
RegularSession();
SelectedAttack = caster.Attacks[0];
} else {
StartHelper();
}
}
private void StartHelper() {
Layout.Clear(true);
Layout.Alignment = TextFlag.Center;
Layout.Add(new StartingPanel(this));
}
private void RegularSession() {
Layout.Clear(true);
Layout.Alignment = TextFlag.None;
controlBar = Layout.Add(new ControlBar(this, this));
timelinePanel = new TimelinePanel(this, this);
inspectPanel = new InspectPanel(this, this);
var splitter = Layout.Add(new Splitter(this));
splitter.AddWidget(timelinePanel);
splitter.AddWidget(inspectPanel);
splitter.SetCollapsible(0, false);
splitter.SetStretch(0, 1);
splitter.SetCollapsible(1, false);
splitter.SetStretch(1, 1);
}
// EVENT OPERATIONS
public void AddNewEvent() {
var popup = new Dialog();
popup.Window.Title = "Create New Event";
popup.Window.Size = new Vector2(400, 100);
// var popup = new PopupWidget(null);
popup.Layout = Layout.Column();
popup.Layout.Margin = 16;
popup.Layout.Spacing = 8;
// popup.Layout.Add(new Label("Create a new event?") { VerticalSizeMode = SizeMode.CanShrink });
var lineEdit = popup.Layout.Add(new LineEdit() { PlaceholderText = "Event name"});
var types = EditorTypeLibrary
.GetTypes(selectedTimeline.EventType)
.Where(x => !x.IsAbstract && !x.HasAttribute<HideAttribute>())
.ToArray();
var typeCombo = popup.Layout.Add(new ComboBox());
foreach (var type in types) {
typeCombo.AddItem(type.Name);
}
var bottomRow = popup.Layout.AddRow();
bottomRow.AddStretchCell();
bottomRow.Add(new Button.Primary("Confirm") {
Clicked = () => {
if (string.IsNullOrWhiteSpace(lineEdit.Text)) {
Log.Error("Invalid event name");
return;
}
AttackEvent evt = types[typeCombo.CurrentIndex].Create<AttackEvent>();
evt.Name = lineEdit.Text;
evt.Time = currentTime;
selectedTimeline.AddEvent(evt);
if (evt is IModifier modifier) {
modifier.OnAdd(selectedAttack);
}
// timelineGraphic.RebuildMarkers();
AttackModified();
popup.Window.Destroy();
}
});
popup.Position = Application.CursorPosition;
popup.ConstrainToScreen();
popup.Show();
}
public void RemoveSelectedEvent() {
if (SelectedEvent == null) return;
if (SelectedEvent is IModifier modifier) {
modifier.OnRemove(selectedAttack);
}
SelectedTimeline.RemoveEvent(SelectedEvent);
SelectedEvent = null;
AttackModified();
}
public void SelectedEventModified() {
if (selectedEvent is IModifier modifier) {
modifier.Modify(selectedAttack);
}
AttackModified(false);
if (caster.InPreviewMode) {
bool resimulate = false;
if (selectedEvent is SpawnEntity spawnEvt) {
caster.RebuildEntity(spawnEvt);
resimulate = true;
} else if (selectedEvent is PatternEvent patternEvt) {
caster.RebuildPreviewEntities(true); // rebuild everything just in case
resimulate = true;
}
if (resimulate) caster.ResimulatePreview(currentTime);
}
}
// SAVING
[Shortcut("editor.save", "Ctrl+S", ShortcutType.Window)]
public void SaveAttack() {
if (!caster.IsValid()) return;
if (selectedAttack == null) return;
// SORT TIMELINES
foreach (var item in availableTimelines) {
item.timeline.Sort();
}
// CALCULATE DURATION
float longestTime = 0;
foreach (var item in availableTimelines) {
if (item.timeline.Events.Count == 0) continue;
longestTime = MathF.Max(longestTime, item.timeline.Events[^1].Time);
}
selectedAttack.CalculatedDuration = longestTime;
// GET RENDERER DATA
Dictionary<GameObject, AttackData.RendererData> cache = new Dictionary<GameObject, AttackData.RendererData>();
foreach (var evt in SelectedAttack.SpawnTimeline.Events) {
if (evt is not SpawnEntity ent) continue;
var renderers = ent.Data.Components
.Where(x => x is RendererDefinition)
.Cast<RendererDefinition>();
if (renderers.Count() == 0) continue;
foreach (var renderer in renderers) {
AttackData.RendererData data;
if (!cache.TryGetValue(renderer.Prefab, out data)) {
data = new AttackData.RendererData();
data.Prefab = renderer.Prefab;
cache.Add(renderer.Prefab, data);
}
data.UseCount++;
}
}
SelectedAttack.RenderPoolingData = cache.Values.ToList();
AttackData reloadedData;
if (!selectedAttack.EmbeddedResource.HasValue) {
var asset = AssetSystem.FindByPath(selectedAttack.ResourcePath);
asset.SaveToDisk(selectedAttack);
reloadedData = asset.LoadResource<AttackData>();
} else {
Log.Warning("Resource is embedded! Can't save directly.");
reloadedData = SelectedAttack;
}
// RELOAD ALL THE UI BECAUSE FUCK IT
// SAVING BREAKS A BUNCH OF REFERENCES FOR SOME REASON
var indexCopy = selectedTimelineIndex.Value;
var idCopy = selectedEventId;
RegularSession();
SelectedAttack = reloadedData;
SelectedTimeline = availableTimelines[indexCopy].timeline;
if (idCopy.HasValue) {
SelectedEvent = selectedTimeline.GetEvent(idCopy.Value);
}
timelinePanel.OnTimelineSelected();
}
// CALLBACKS
public void AttackModified(bool allowSave = true) {
selectedAttack.StateHasChanged();
if (allowSave && autosave) {
SaveAttack();
}
timelinePanel.OnAttackModified();
}
private void OnAttackChanged() {
if (SelectedAttack != null) {
availableTimelines = selectedAttack.GetType().GetProperties()
.Where(x => x.PropertyType.IsAssignableTo(typeof(ITimeline)))
.Index()
.Select(x => new TimelineInfo() {
name = x.Item.Name,
index = x.Index,
timeline = (ITimeline)x.Item.GetValue(selectedAttack)
}).ToArray();
SelectedTimeline = availableTimelines[0].timeline;
} else {
availableTimelines = [];
SelectedTimeline = null;
}
timelinePanel.OnAttackSelected();
}
}
}