Replicated the weapon flying tick setup using resources
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that binds a node input property to an activation data field. Uses a two-step selection: first
|
||||
/// select the <see cref="IActivationDataProvider"/> implementation, then select a compatible field from that provider.
|
||||
/// Providers are discovered via reflection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A graph supports only one activation data provider. Once any other node in the graph references a provider, the
|
||||
/// provider dropdown is locked to that provider. The user only needs to clear the bindings on other nodes to unlock
|
||||
/// the dropdown.
|
||||
/// </remarks>
|
||||
[Tool]
|
||||
internal sealed partial class ActivationDataResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly List<string> _providerClassNames = [];
|
||||
private readonly List<string> _fieldNames = [];
|
||||
|
||||
private StatescriptGraph? _graph;
|
||||
private StatescriptNodeProperty? _currentProperty;
|
||||
|
||||
private OptionButton? _providerDropdown;
|
||||
private OptionButton? _fieldDropdown;
|
||||
private Action? _onChanged;
|
||||
private Type _expectedType = typeof(Variant128);
|
||||
|
||||
private string _selectedProviderClassName = string.Empty;
|
||||
private string _selectedFieldName = string.Empty;
|
||||
private StatescriptVariableType _selectedFieldType = StatescriptVariableType.Int;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Activation Data";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "ActivationData";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
_expectedType = expectedType;
|
||||
_graph = graph;
|
||||
_currentProperty = property;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
if (property?.Resolver is ActivationDataResolverResource activationRes)
|
||||
{
|
||||
_selectedProviderClassName = activationRes.ProviderClassName;
|
||||
_selectedFieldName = activationRes.FieldName;
|
||||
_selectedFieldType = activationRes.FieldType;
|
||||
}
|
||||
|
||||
var providerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(providerRow);
|
||||
|
||||
providerRow.AddChild(new Label
|
||||
{
|
||||
Text = "Provider:",
|
||||
CustomMinimumSize = new Vector2(75, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_providerDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateProviderDropdown();
|
||||
providerRow.AddChild(_providerDropdown);
|
||||
|
||||
// Re-scan the graph each time the dropdown opens to pick up changes from other editors.
|
||||
_providerDropdown.GetPopup().AboutToPopup += PopulateProviderDropdown;
|
||||
|
||||
var fieldRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(fieldRow);
|
||||
|
||||
fieldRow.AddChild(new Label
|
||||
{
|
||||
Text = "Field:",
|
||||
CustomMinimumSize = new Vector2(75, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_fieldDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateFieldDropdown();
|
||||
fieldRow.AddChild(_fieldDropdown);
|
||||
|
||||
_providerDropdown.ItemSelected += OnProviderDropdownItemSelected;
|
||||
_fieldDropdown.ItemSelected += OnFieldDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new ActivationDataResolverResource
|
||||
{
|
||||
ProviderClassName = _selectedProviderClassName,
|
||||
FieldName = _selectedFieldName,
|
||||
FieldType = _selectedFieldType,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private static string FindExistingProvider(StatescriptGraph graph, StatescriptNodeProperty? currentProperty)
|
||||
{
|
||||
foreach (StatescriptNode node in graph.Nodes)
|
||||
{
|
||||
foreach (StatescriptNodeProperty binding in node.PropertyBindings)
|
||||
{
|
||||
// Skip the property we're currently editing — the user should be free to change it.
|
||||
if (ReferenceEquals(binding, currentProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (binding.Resolver is ActivationDataResolverResource { ProviderClassName.Length: > 0 } resolver)
|
||||
{
|
||||
return resolver.ProviderClassName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static IActivationDataProvider? InstantiateProvider(string className)
|
||||
{
|
||||
if (string.IsNullOrEmpty(className))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type? type = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.FirstOrDefault(
|
||||
x => typeof(IActivationDataProvider).IsAssignableFrom(x)
|
||||
&& !x.IsAbstract
|
||||
&& !x.IsInterface
|
||||
&& x.Name == className);
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Activator.CreateInstance(type) as IActivationDataProvider;
|
||||
}
|
||||
|
||||
private void OnProviderDropdownItemSelected(long index)
|
||||
{
|
||||
if (_providerDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _providerDropdown.Selected;
|
||||
_selectedProviderClassName = idx >= 0 && idx < _providerClassNames.Count
|
||||
? _providerClassNames[idx]
|
||||
: string.Empty;
|
||||
_selectedFieldName = string.Empty;
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
|
||||
PopulateFieldDropdown();
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnFieldDropdownItemSelected(long index)
|
||||
{
|
||||
if (_fieldDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dropdownIndex = _fieldDropdown.Selected;
|
||||
|
||||
if (dropdownIndex >= 0 && dropdownIndex < _fieldNames.Count)
|
||||
{
|
||||
_selectedFieldName = _fieldNames[dropdownIndex];
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedFieldName))
|
||||
{
|
||||
ResolveFieldType();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedFieldName = string.Empty;
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateProviderDropdown()
|
||||
{
|
||||
if (_providerDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_providerDropdown.Clear();
|
||||
_providerClassNames.Clear();
|
||||
|
||||
// Always add a (None) option to allow deselecting.
|
||||
_providerDropdown.AddItem("(None)");
|
||||
_providerClassNames.Add(string.Empty);
|
||||
|
||||
// Re-scan the graph each time to pick up changes from other editors.
|
||||
var graphLockedProvider = _graph is not null
|
||||
? FindExistingProvider(_graph, _currentProperty)
|
||||
: string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(graphLockedProvider))
|
||||
{
|
||||
// Another node already uses a provider: only show that one (plus None).
|
||||
_providerDropdown.AddItem(graphLockedProvider);
|
||||
_providerClassNames.Add(graphLockedProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var name in AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(a => a.GetTypes())
|
||||
.Where(x => typeof(IActivationDataProvider).IsAssignableFrom(x)
|
||||
&& !x.IsAbstract
|
||||
&& !x.IsInterface)
|
||||
.Select(x => x.Name))
|
||||
{
|
||||
_providerDropdown.AddItem(name);
|
||||
_providerClassNames.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
if (!string.IsNullOrEmpty(_selectedProviderClassName))
|
||||
{
|
||||
for (var i = 0; i < _providerClassNames.Count; i++)
|
||||
{
|
||||
if (_providerClassNames[i] == _selectedProviderClassName)
|
||||
{
|
||||
_providerDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to (None).
|
||||
_providerDropdown.Selected = 0;
|
||||
_selectedProviderClassName = string.Empty;
|
||||
}
|
||||
|
||||
private void PopulateFieldDropdown()
|
||||
{
|
||||
if (_fieldDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fieldDropdown.Clear();
|
||||
_fieldNames.Clear();
|
||||
|
||||
// Always add a (None) option.
|
||||
_fieldDropdown.AddItem("(None)");
|
||||
_fieldNames.Add(string.Empty);
|
||||
|
||||
IActivationDataProvider? provider = InstantiateProvider(_selectedProviderClassName);
|
||||
|
||||
if (provider is not null)
|
||||
{
|
||||
foreach (ForgeActivationDataField field in provider.GetFields())
|
||||
{
|
||||
if (string.IsNullOrEmpty(field.FieldName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_expectedType != typeof(Variant128)
|
||||
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, field.FieldType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_fieldDropdown.AddItem(field.FieldName);
|
||||
_fieldNames.Add(field.FieldName);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
if (!string.IsNullOrEmpty(_selectedFieldName))
|
||||
{
|
||||
for (var i = 0; i < _fieldNames.Count; i++)
|
||||
{
|
||||
if (_fieldNames[i] == _selectedFieldName)
|
||||
{
|
||||
_fieldDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to (None).
|
||||
_fieldDropdown.Selected = 0;
|
||||
_selectedFieldName = string.Empty;
|
||||
}
|
||||
|
||||
private void ResolveFieldType()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedProviderClassName) || string.IsNullOrEmpty(_selectedFieldName))
|
||||
{
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
IActivationDataProvider? provider = InstantiateProvider(_selectedProviderClassName);
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ForgeActivationDataField field in provider.GetFields())
|
||||
{
|
||||
if (field.FieldName == _selectedFieldName)
|
||||
{
|
||||
_selectedFieldType = field.FieldType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedFieldType = StatescriptVariableType.Int;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cvegkmbda17em
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that reads a value from a Forge entity attribute. Shows attribute set and attribute dropdowns.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class AttributeResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private OptionButton? _setDropdown;
|
||||
private OptionButton? _attributeDropdown;
|
||||
private string _selectedSetClass = string.Empty;
|
||||
private string _selectedAttribute = string.Empty;
|
||||
private Action? _onChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Attribute";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Attribute";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(int) || expectedType == typeof(Variant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
if (property?.Resolver is AttributeResolverResource attrRes)
|
||||
{
|
||||
_selectedSetClass = attrRes.AttributeSetClass;
|
||||
_selectedAttribute = attrRes.AttributeName;
|
||||
}
|
||||
|
||||
var setRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(setRow);
|
||||
|
||||
setRow.AddChild(new Label
|
||||
{
|
||||
Text = "Set:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_setDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateSetDropdown();
|
||||
setRow.AddChild(_setDropdown);
|
||||
|
||||
var attrRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(attrRow);
|
||||
|
||||
attrRow.AddChild(new Label
|
||||
{
|
||||
Text = "Attr:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_attributeDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateAttributeDropdown();
|
||||
attrRow.AddChild(_attributeDropdown);
|
||||
|
||||
_setDropdown.ItemSelected += OnSetDropdownItemSelected;
|
||||
_attributeDropdown.ItemSelected += OnAttributeDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new AttributeResolverResource
|
||||
{
|
||||
AttributeSetClass = _selectedSetClass,
|
||||
AttributeName = _selectedAttribute,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private void OnSetDropdownItemSelected(long index)
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedSetClass = _setDropdown.GetItemText(_setDropdown.Selected);
|
||||
_selectedAttribute = string.Empty;
|
||||
PopulateAttributeDropdown();
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnAttributeDropdownItemSelected(long index)
|
||||
{
|
||||
if (_attributeDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedAttribute = _attributeDropdown.GetItemText(_attributeDropdown.Selected);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateSetDropdown()
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_setDropdown.Clear();
|
||||
|
||||
foreach (var option in EditorUtils.GetAttributeSetOptions())
|
||||
{
|
||||
_setDropdown.AddItem(option);
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
if (!string.IsNullOrEmpty(_selectedSetClass))
|
||||
{
|
||||
for (var i = 0; i < _setDropdown.GetItemCount(); i++)
|
||||
{
|
||||
if (_setDropdown.GetItemText(i) == _selectedSetClass)
|
||||
{
|
||||
_setDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to first if available.
|
||||
if (_setDropdown.GetItemCount() > 0)
|
||||
{
|
||||
_setDropdown.Selected = 0;
|
||||
_selectedSetClass = _setDropdown.GetItemText(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateAttributeDropdown()
|
||||
{
|
||||
if (_attributeDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_attributeDropdown.Clear();
|
||||
|
||||
foreach (var option in EditorUtils.GetAttributeOptions(_selectedSetClass))
|
||||
{
|
||||
_attributeDropdown.AddItem(option);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedAttribute))
|
||||
{
|
||||
for (var i = 0; i < _attributeDropdown.GetItemCount(); i++)
|
||||
{
|
||||
if (_attributeDropdown.GetItemText(i) == _selectedAttribute)
|
||||
{
|
||||
_attributeDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_attributeDropdown.GetItemCount() > 0)
|
||||
{
|
||||
_attributeDropdown.Selected = 0;
|
||||
_selectedAttribute = _attributeDropdown.GetItemText(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://ciagvn5l8gnbq
|
||||
@@ -0,0 +1,286 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript.Properties;
|
||||
using Godot;
|
||||
using ForgeVariant128 = Gamesmiths.Forge.Statescript.Variant128;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that compares two nested numeric resolvers and produces a boolean result. Supports nesting any
|
||||
/// numeric-compatible resolver as left/right operands, enabling powerful comparisons like "Attribute > Constant".
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class ComparisonResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private StatescriptGraph? _graph;
|
||||
private Action? _onChanged;
|
||||
|
||||
private OptionButton? _operationDropdown;
|
||||
private VBoxContainer? _leftContainer;
|
||||
private VBoxContainer? _rightContainer;
|
||||
private OptionButton? _leftResolverDropdown;
|
||||
private OptionButton? _rightResolverDropdown;
|
||||
|
||||
private NodeEditorProperty? _leftEditor;
|
||||
private NodeEditorProperty? _rightEditor;
|
||||
|
||||
private List<Func<NodeEditorProperty>> _numericFactories = [];
|
||||
private ComparisonOperation _operation;
|
||||
|
||||
private VBoxContainer? _leftEditorContainer;
|
||||
private VBoxContainer? _rightEditorContainer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Comparison";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Comparison";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(bool) || expectedType == typeof(ForgeVariant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_graph = graph;
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
_numericFactories = StatescriptResolverRegistry.GetCompatibleFactories(typeof(ForgeVariant128));
|
||||
|
||||
_numericFactories.RemoveAll(x =>
|
||||
{
|
||||
using NodeEditorProperty temp = x();
|
||||
return temp.ResolverTypeId == "Comparison";
|
||||
});
|
||||
|
||||
var comparisonResolver = property?.Resolver as ComparisonResolverResource;
|
||||
|
||||
if (comparisonResolver is not null)
|
||||
{
|
||||
_operation = comparisonResolver.Operation;
|
||||
}
|
||||
|
||||
var leftFoldable = new FoldableContainer { Title = "Left:" };
|
||||
leftFoldable.FoldingChanged += OnFoldingChanged;
|
||||
vBox.AddChild(leftFoldable);
|
||||
|
||||
_leftContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
leftFoldable.AddChild(_leftContainer);
|
||||
|
||||
_leftEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_leftResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Left);
|
||||
_leftContainer.AddChild(_leftResolverDropdown);
|
||||
_leftContainer.AddChild(_leftEditorContainer);
|
||||
ShowNestedEditor(
|
||||
GetSelectedIndex(comparisonResolver?.Left),
|
||||
comparisonResolver?.Left,
|
||||
_leftEditorContainer,
|
||||
x => _leftEditor = x);
|
||||
|
||||
_leftResolverDropdown.ItemSelected += OnLeftResolverDropdownItemSelected;
|
||||
|
||||
var opRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(opRow);
|
||||
|
||||
opRow.AddChild(new Label { Text = "Op:" });
|
||||
|
||||
_operationDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
_operationDropdown.AddItem("==");
|
||||
_operationDropdown.AddItem("!=");
|
||||
_operationDropdown.AddItem("<");
|
||||
_operationDropdown.AddItem("<=");
|
||||
_operationDropdown.AddItem(">");
|
||||
_operationDropdown.AddItem(">=");
|
||||
|
||||
_operationDropdown.Selected = (int)_operation;
|
||||
|
||||
_operationDropdown.ItemSelected += OnOperationDropdownItemSelected;
|
||||
|
||||
opRow.AddChild(_operationDropdown);
|
||||
|
||||
var rightFoldable = new FoldableContainer { Title = "Right:" };
|
||||
rightFoldable.FoldingChanged += OnFoldingChanged;
|
||||
vBox.AddChild(rightFoldable);
|
||||
|
||||
_rightContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
rightFoldable.AddChild(_rightContainer);
|
||||
|
||||
_rightEditorContainer = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_rightResolverDropdown = CreateResolverDropdownControl(comparisonResolver?.Right);
|
||||
_rightContainer.AddChild(_rightResolverDropdown);
|
||||
_rightContainer.AddChild(_rightEditorContainer);
|
||||
ShowNestedEditor(
|
||||
GetSelectedIndex(comparisonResolver?.Right),
|
||||
comparisonResolver?.Right,
|
||||
_rightEditorContainer,
|
||||
x => _rightEditor = x);
|
||||
|
||||
_rightResolverDropdown.ItemSelected += OnRightResolverDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
var comparisonResolver = new ComparisonResolverResource { Operation = _operation };
|
||||
|
||||
if (_leftEditor is not null)
|
||||
{
|
||||
var leftProperty = new StatescriptNodeProperty();
|
||||
_leftEditor.SaveTo(leftProperty);
|
||||
comparisonResolver.Left = leftProperty.Resolver;
|
||||
}
|
||||
|
||||
if (_rightEditor is not null)
|
||||
{
|
||||
var rightProperty = new StatescriptNodeProperty();
|
||||
_rightEditor.SaveTo(rightProperty);
|
||||
comparisonResolver.Right = rightProperty.Resolver;
|
||||
}
|
||||
|
||||
property.Resolver = comparisonResolver;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
|
||||
_leftEditor?.ClearCallbacks();
|
||||
_rightEditor?.ClearCallbacks();
|
||||
}
|
||||
|
||||
private void OnFoldingChanged(bool isFolded)
|
||||
{
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private void OnOperationDropdownItemSelected(long x)
|
||||
{
|
||||
_operation = (ComparisonOperation)(int)x;
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnLeftResolverDropdownItemSelected(long x)
|
||||
{
|
||||
HandleResolverDropdownChanged((int)x, _leftEditorContainer, editor => _leftEditor = editor);
|
||||
}
|
||||
|
||||
private void OnRightResolverDropdownItemSelected(long x)
|
||||
{
|
||||
HandleResolverDropdownChanged((int)x, _rightEditorContainer, editor => _rightEditor = editor);
|
||||
}
|
||||
|
||||
private void HandleResolverDropdownChanged(
|
||||
int selectedIndex,
|
||||
VBoxContainer? editorContainer,
|
||||
Action<NodeEditorProperty?> setEditor)
|
||||
{
|
||||
if (editorContainer is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node child in editorContainer.GetChildren())
|
||||
{
|
||||
editorContainer.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
|
||||
setEditor(null);
|
||||
ShowNestedEditor(selectedIndex, null, editorContainer, setEditor);
|
||||
_onChanged?.Invoke();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private int GetSelectedIndex(StatescriptResolverResource? existingResolver)
|
||||
{
|
||||
var selectedIndex = 0;
|
||||
|
||||
if (existingResolver is not null)
|
||||
{
|
||||
var existingTypeId = existingResolver.ResolverTypeId;
|
||||
|
||||
for (var i = 0; i < _numericFactories.Count; i++)
|
||||
{
|
||||
using NodeEditorProperty temp = _numericFactories[i]();
|
||||
|
||||
if (temp.ResolverTypeId == existingTypeId)
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
private OptionButton CreateResolverDropdownControl(StatescriptResolverResource? existingResolver)
|
||||
{
|
||||
var dropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
foreach (Func<NodeEditorProperty> factory in _numericFactories)
|
||||
{
|
||||
using NodeEditorProperty temp = factory();
|
||||
dropdown.AddItem(temp.DisplayName);
|
||||
}
|
||||
|
||||
dropdown.Selected = GetSelectedIndex(existingResolver);
|
||||
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
private void ShowNestedEditor(
|
||||
int factoryIndex,
|
||||
StatescriptResolverResource? existingResolver,
|
||||
VBoxContainer container,
|
||||
Action<NodeEditorProperty?> setEditor)
|
||||
{
|
||||
if (_graph is null || factoryIndex < 0 || factoryIndex >= _numericFactories.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
NodeEditorProperty editor = _numericFactories[factoryIndex]();
|
||||
|
||||
StatescriptNodeProperty? tempProperty = null;
|
||||
|
||||
if (existingResolver is not null)
|
||||
{
|
||||
tempProperty = new StatescriptNodeProperty { Resolver = existingResolver };
|
||||
}
|
||||
|
||||
editor.Setup(_graph, tempProperty, typeof(ForgeVariant128), OnNestedEditorChanged, false);
|
||||
|
||||
editor.LayoutSizeChanged += RaiseLayoutSizeChanged;
|
||||
|
||||
container.AddChild(editor);
|
||||
setEditor(editor);
|
||||
}
|
||||
|
||||
private void OnNestedEditorChanged()
|
||||
{
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8uywbj8s8brq
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor for the ability activation magnitude. No configuration is needed, it simply reads the magnitude from
|
||||
/// the <see cref="Abilities.AbilityBehaviorContext"/> at runtime. Only compatible with <see langword="float"/> inputs.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class MagnitudeResolverEditor : NodeEditorProperty
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Magnitude";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Magnitude";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(float) || expectedType == typeof(Variant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Text = "Ability Magnitude",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
AddChild(label);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new MagnitudeResolverResource();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cdsw31atjur88
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that binds a node input property to a shared variable on the owning entity. Uses a two-step
|
||||
/// selection: first select the <see cref="ForgeSharedVariableSet"/> resource, then select a compatible variable from
|
||||
/// that set. At runtime the value is read from the entity's <see cref="GraphContext.SharedVariables"/> bag.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class SharedVariableResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly List<string> _setPaths = [];
|
||||
private readonly List<string> _setDisplayNames = [];
|
||||
private readonly List<string> _variableNames = [];
|
||||
|
||||
private OptionButton? _setDropdown;
|
||||
private OptionButton? _variableDropdown;
|
||||
private Action? _onChanged;
|
||||
private Type _expectedType = typeof(Variant128);
|
||||
|
||||
private string _selectedSetPath = string.Empty;
|
||||
private string _selectedVariableName = string.Empty;
|
||||
private StatescriptVariableType _selectedVariableType = StatescriptVariableType.Int;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Shared Variable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "SharedVariable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
_expectedType = expectedType;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
if (property?.Resolver is SharedVariableResolverResource sharedRes)
|
||||
{
|
||||
_selectedSetPath = sharedRes.SharedVariableSetPath;
|
||||
_selectedVariableName = sharedRes.VariableName;
|
||||
_selectedVariableType = sharedRes.VariableType;
|
||||
}
|
||||
|
||||
var setRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(setRow);
|
||||
|
||||
setRow.AddChild(new Label
|
||||
{
|
||||
Text = "Set:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_setDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateSetDropdown();
|
||||
setRow.AddChild(_setDropdown);
|
||||
|
||||
var varRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(varRow);
|
||||
|
||||
varRow.AddChild(new Label
|
||||
{
|
||||
Text = "Var:",
|
||||
CustomMinimumSize = new Vector2(45, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
});
|
||||
|
||||
_variableDropdown = new OptionButton { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
PopulateVariableDropdown();
|
||||
varRow.AddChild(_variableDropdown);
|
||||
|
||||
_setDropdown.ItemSelected += OnSetDropdownItemSelected;
|
||||
_variableDropdown.ItemSelected += OnVariableDropdownItemSelected;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new SharedVariableResolverResource
|
||||
{
|
||||
SharedVariableSetPath = _selectedSetPath,
|
||||
VariableName = _selectedVariableName,
|
||||
VariableType = _selectedVariableType,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private static List<string> FindAllSharedVariableSetPaths()
|
||||
{
|
||||
var results = new List<string>();
|
||||
EditorFileSystemDirectory root = EditorInterface.Singleton.GetResourceFilesystem().GetFilesystem();
|
||||
ScanFilesystemDirectory(root, results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void ScanFilesystemDirectory(EditorFileSystemDirectory dir, List<string> results)
|
||||
{
|
||||
for (var i = 0; i < dir.GetFileCount(); i++)
|
||||
{
|
||||
var path = dir.GetFilePath(i);
|
||||
|
||||
if (!path.EndsWith(".tres", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& !path.EndsWith(".res", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Resource resource = ResourceLoader.Load(path);
|
||||
|
||||
if (resource is ForgeSharedVariableSet)
|
||||
{
|
||||
results.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < dir.GetSubdirCount(); i++)
|
||||
{
|
||||
ScanFilesystemDirectory(dir.GetSubdir(i), results);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSetDropdownItemSelected(long index)
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _setDropdown.Selected;
|
||||
_selectedSetPath = idx >= 0 && idx < _setPaths.Count ? _setPaths[idx] : string.Empty;
|
||||
_selectedVariableName = string.Empty;
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
|
||||
PopulateVariableDropdown();
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnVariableDropdownItemSelected(long index)
|
||||
{
|
||||
if (_variableDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _variableDropdown.Selected;
|
||||
|
||||
if (idx >= 0 && idx < _variableNames.Count)
|
||||
{
|
||||
_selectedVariableName = _variableNames[idx];
|
||||
ResolveVariableType();
|
||||
}
|
||||
else
|
||||
{
|
||||
_selectedVariableName = string.Empty;
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateSetDropdown()
|
||||
{
|
||||
if (_setDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_setDropdown.Clear();
|
||||
_setPaths.Clear();
|
||||
_setDisplayNames.Clear();
|
||||
|
||||
_setDropdown.AddItem("(None)");
|
||||
_setPaths.Add(string.Empty);
|
||||
_setDisplayNames.Add("(None)");
|
||||
|
||||
foreach (var path in FindAllSharedVariableSetPaths())
|
||||
{
|
||||
var displayName = path[(path.LastIndexOf('/') + 1)..];
|
||||
|
||||
if (displayName.EndsWith(".tres", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
displayName = displayName[..^5];
|
||||
}
|
||||
|
||||
_setDropdown.AddItem(displayName);
|
||||
_setPaths.Add(path);
|
||||
_setDisplayNames.Add(displayName);
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
for (var i = 0; i < _setPaths.Count; i++)
|
||||
{
|
||||
if (_setPaths[i] == _selectedSetPath)
|
||||
{
|
||||
_setDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_setDropdown.Selected = 0;
|
||||
_selectedSetPath = string.Empty;
|
||||
}
|
||||
|
||||
private void PopulateVariableDropdown()
|
||||
{
|
||||
if (_variableDropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_variableDropdown.Clear();
|
||||
_variableNames.Clear();
|
||||
|
||||
_variableDropdown.AddItem("(None)");
|
||||
_variableNames.Add(string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(_selectedSetPath) && ResourceLoader.Exists(_selectedSetPath))
|
||||
{
|
||||
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
|
||||
|
||||
if (set is not null)
|
||||
{
|
||||
foreach (ForgeSharedVariableDefinition def in set.Variables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(def.VariableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_expectedType != typeof(Variant128)
|
||||
&& !StatescriptVariableTypeConverter.IsCompatible(_expectedType, def.VariableType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var label = $"{def.VariableName}";
|
||||
_variableDropdown.AddItem(label);
|
||||
_variableNames.Add(def.VariableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection.
|
||||
for (var i = 0; i < _variableNames.Count; i++)
|
||||
{
|
||||
if (_variableNames[i] == _selectedVariableName)
|
||||
{
|
||||
_variableDropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_variableDropdown.Selected = 0;
|
||||
_selectedVariableName = string.Empty;
|
||||
}
|
||||
|
||||
private void ResolveVariableType()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_selectedSetPath)
|
||||
|| string.IsNullOrEmpty(_selectedVariableName)
|
||||
|| !ResourceLoader.Exists(_selectedSetPath))
|
||||
{
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
ForgeSharedVariableSet? set = ResourceLoader.Load<ForgeSharedVariableSet>(_selectedSetPath);
|
||||
|
||||
if (set is null)
|
||||
{
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ForgeSharedVariableDefinition def in set.Variables)
|
||||
{
|
||||
if (def.VariableName == _selectedVariableName)
|
||||
{
|
||||
_selectedVariableType = def.VariableType;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedVariableType = StatescriptVariableType.Int;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://55ynvr5cbscp
|
||||
198
addons/forge/editor/statescript/resolvers/TagResolverEditor.cs
Normal file
198
addons/forge/editor/statescript/resolvers/TagResolverEditor.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Core;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Gamesmiths.Forge.Statescript;
|
||||
using Gamesmiths.Forge.Tags;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that selects a single tag. Reuses the tag tree UI pattern from <c>TagEditorProperty</c>.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class TagResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly Dictionary<TreeItem, TagNode> _treeItemToNode = [];
|
||||
|
||||
private Button? _tagButton;
|
||||
private ScrollContainer? _scroll;
|
||||
private Tree? _tree;
|
||||
private string _selectedTag = string.Empty;
|
||||
private Texture2D? _checkedIcon;
|
||||
private Texture2D? _uncheckedIcon;
|
||||
private Action? _onChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Tag";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Tag";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return expectedType == typeof(bool) || expectedType == typeof(Variant128);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
AddChild(vBox);
|
||||
|
||||
// Restore from existing binding.
|
||||
if (property?.Resolver is TagResolverResource tagRes)
|
||||
{
|
||||
_selectedTag = tagRes.Tag;
|
||||
}
|
||||
|
||||
_checkedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiRadioChecked", "EditorIcons");
|
||||
|
||||
_uncheckedIcon = EditorInterface.Singleton
|
||||
.GetEditorTheme()
|
||||
.GetIcon("GuiRadioUnchecked", "EditorIcons");
|
||||
|
||||
_tagButton = new Button
|
||||
{
|
||||
ToggleMode = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Text = string.IsNullOrEmpty(_selectedTag) ? "(select tag)" : _selectedTag,
|
||||
};
|
||||
|
||||
_tagButton.Toggled += OnTagButtonToggled;
|
||||
|
||||
vBox.AddChild(_tagButton);
|
||||
|
||||
_scroll = new ScrollContainer
|
||||
{
|
||||
Visible = false,
|
||||
CustomMinimumSize = new Vector2(0, 180),
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_tree = new Tree
|
||||
{
|
||||
HideRoot = true,
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||
};
|
||||
|
||||
_scroll.AddChild(_tree);
|
||||
vBox.AddChild(_scroll);
|
||||
|
||||
_tree.ButtonClicked += OnTreeButtonClicked;
|
||||
|
||||
RebuildTree();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new TagResolverResource
|
||||
{
|
||||
Tag = _selectedTag,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private void OnTagButtonToggled(bool toggled)
|
||||
{
|
||||
if (_scroll is not null)
|
||||
{
|
||||
_scroll.Visible = toggled;
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTreeButtonClicked(TreeItem item, long column, long id, long mouseButton)
|
||||
{
|
||||
if (mouseButton != 1 || id != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_treeItemToNode.TryGetValue(item, out TagNode? tagNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Forge.Core.StringKey newValue = tagNode.CompleteTagKey;
|
||||
|
||||
if (newValue == _selectedTag)
|
||||
{
|
||||
newValue = string.Empty;
|
||||
}
|
||||
|
||||
_selectedTag = newValue;
|
||||
|
||||
if (_tagButton is not null)
|
||||
{
|
||||
_tagButton.Text = string.IsNullOrEmpty(_selectedTag) ? "(select tag)" : _selectedTag;
|
||||
}
|
||||
|
||||
RebuildTree();
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void RebuildTree()
|
||||
{
|
||||
if (_tree is null || _checkedIcon is null || _uncheckedIcon is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_tree.Clear();
|
||||
_treeItemToNode.Clear();
|
||||
|
||||
TreeItem root = _tree.CreateItem();
|
||||
|
||||
ForgeData forgePluginData = ResourceLoader.Load<ForgeData>(ForgeData.ForgeDataResourcePath);
|
||||
var tagsManager = new TagsManager([.. forgePluginData.RegisteredTags]);
|
||||
|
||||
BuildTreeRecursive(root, tagsManager.RootNode);
|
||||
}
|
||||
|
||||
private void BuildTreeRecursive(TreeItem parent, TagNode node)
|
||||
{
|
||||
if (_tree is null || _checkedIcon is null || _uncheckedIcon is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (TagNode child in node.ChildTags)
|
||||
{
|
||||
TreeItem item = _tree.CreateItem(parent);
|
||||
item.SetText(0, child.TagKey);
|
||||
|
||||
var selected = _selectedTag == child.CompleteTagKey;
|
||||
item.AddButton(0, selected ? _checkedIcon : _uncheckedIcon);
|
||||
|
||||
_treeItemToNode[item] = child;
|
||||
BuildTreeRecursive(item, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://drl073r6x5m16
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that binds an input property to a graph variable. Only variables whose type is compatible with the
|
||||
/// expected type are shown in the dropdown.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class VariableResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private readonly List<string> _variableNames = [];
|
||||
|
||||
private OptionButton? _dropdown;
|
||||
private string _selectedVariableName = string.Empty;
|
||||
private Action? _onChanged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Variable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Variable";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_onChanged = onChanged;
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||
CustomMinimumSize = new Vector2(200, 25);
|
||||
|
||||
_dropdown = new OptionButton
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
CustomMinimumSize = new Vector2(100, 0),
|
||||
};
|
||||
|
||||
_dropdown.SetMeta("is_variable_dropdown", true);
|
||||
|
||||
PopulateDropdown(graph, expectedType);
|
||||
|
||||
if (property?.Resolver is VariableResolverResource varRes
|
||||
&& !string.IsNullOrEmpty(varRes.VariableName))
|
||||
{
|
||||
_selectedVariableName = varRes.VariableName;
|
||||
SelectByName(varRes.VariableName);
|
||||
}
|
||||
|
||||
_dropdown.ItemSelected += OnDropdownItemSelected;
|
||||
|
||||
AddChild(_dropdown);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
property.Resolver = new VariableResolverResource
|
||||
{
|
||||
VariableName = _selectedVariableName,
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private void OnDropdownItemSelected(long index)
|
||||
{
|
||||
if (_dropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = _dropdown.Selected;
|
||||
_selectedVariableName = idx >= 0 && idx < _variableNames.Count ? _variableNames[idx] : string.Empty;
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void PopulateDropdown(StatescriptGraph graph, Type expectedType)
|
||||
{
|
||||
if (_dropdown is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dropdown.Clear();
|
||||
_variableNames.Clear();
|
||||
|
||||
_dropdown.AddItem("(None)");
|
||||
_variableNames.Add(string.Empty);
|
||||
|
||||
foreach (StatescriptGraphVariable variable in graph.Variables)
|
||||
{
|
||||
if (string.IsNullOrEmpty(variable.VariableName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!StatescriptVariableTypeConverter.IsCompatible(expectedType, variable.VariableType))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_dropdown.AddItem(variable.VariableName);
|
||||
_variableNames.Add(variable.VariableName);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectByName(string name)
|
||||
{
|
||||
if (_dropdown is null || string.IsNullOrEmpty(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < _variableNames.Count; i++)
|
||||
{
|
||||
if (_variableNames[i] == name)
|
||||
{
|
||||
_dropdown.Selected = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_selectedVariableName = string.Empty;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://cs7v6x0xv1a3k
|
||||
@@ -0,0 +1,388 @@
|
||||
// Copyright © Gamesmiths Guild.
|
||||
|
||||
#if TOOLS
|
||||
using System;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript;
|
||||
using Gamesmiths.Forge.Godot.Resources.Statescript.Resolvers;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using GodotVariant = Godot.Variant;
|
||||
using GodotVector2 = Godot.Vector2;
|
||||
|
||||
namespace Gamesmiths.Forge.Godot.Editor.Statescript.Resolvers;
|
||||
|
||||
/// <summary>
|
||||
/// Resolver editor that holds a constant (inline) value. The user edits the value directly in the node.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
internal sealed partial class VariantResolverEditor : NodeEditorProperty
|
||||
{
|
||||
private StatescriptVariableType _valueType;
|
||||
private bool _isArray;
|
||||
private bool _isArrayExpanded;
|
||||
private GodotVariant _currentValue;
|
||||
private Array<GodotVariant> _arrayValues = [];
|
||||
private Action? _onChanged;
|
||||
|
||||
private Button? _toggleButton;
|
||||
private VBoxContainer? _elementsContainer;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string DisplayName => "Constant";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ResolverTypeId => "Variant";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsCompatibleWith(Type expectedType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Setup(
|
||||
StatescriptGraph graph,
|
||||
StatescriptNodeProperty? property,
|
||||
Type expectedType,
|
||||
Action onChanged,
|
||||
bool isArray)
|
||||
{
|
||||
_isArray = isArray;
|
||||
_onChanged = onChanged;
|
||||
|
||||
if (!StatescriptVariableTypeConverter.TryFromSystemType(expectedType, out _valueType))
|
||||
{
|
||||
_valueType = StatescriptVariableType.Int;
|
||||
}
|
||||
|
||||
if (property?.Resolver is VariantResolverResource variantRes)
|
||||
{
|
||||
_valueType = variantRes.ValueType;
|
||||
|
||||
if (_isArray)
|
||||
{
|
||||
_arrayValues = [.. variantRes.ArrayValues];
|
||||
_isArrayExpanded = variantRes.IsArrayExpanded;
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentValue = variantRes.Value;
|
||||
}
|
||||
}
|
||||
else if (_isArray)
|
||||
{
|
||||
_arrayValues = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType);
|
||||
}
|
||||
|
||||
CustomMinimumSize = new GodotVector2(200, 40);
|
||||
|
||||
if (_isArray)
|
||||
{
|
||||
VBoxContainer arrayEditor = CreateArrayEditor();
|
||||
AddChild(arrayEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
Control valueEditor = CreateValueEditor();
|
||||
AddChild(valueEditor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void SaveTo(StatescriptNodeProperty property)
|
||||
{
|
||||
if (_isArray)
|
||||
{
|
||||
property.Resolver = new VariantResolverResource
|
||||
{
|
||||
ValueType = _valueType,
|
||||
IsArray = true,
|
||||
ArrayValues = [.. _arrayValues],
|
||||
IsArrayExpanded = _isArrayExpanded,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
property.Resolver = new VariantResolverResource
|
||||
{
|
||||
Value = _currentValue,
|
||||
ValueType = _valueType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void ClearCallbacks()
|
||||
{
|
||||
base.ClearCallbacks();
|
||||
_onChanged = null;
|
||||
}
|
||||
|
||||
private Control CreateValueEditor()
|
||||
{
|
||||
if (_valueType == StatescriptVariableType.Bool)
|
||||
{
|
||||
return StatescriptEditorControls.CreateBoolEditor(_currentValue.AsBool(), OnBoolValueChanged);
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsIntegerType(_valueType)
|
||||
|| StatescriptEditorControls.IsFloatType(_valueType))
|
||||
{
|
||||
return StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
_valueType,
|
||||
_currentValue.AsDouble(),
|
||||
OnNumericValueChanged);
|
||||
}
|
||||
|
||||
if (StatescriptEditorControls.IsVectorType(_valueType))
|
||||
{
|
||||
return StatescriptEditorControls.CreateVectorEditor(
|
||||
_valueType,
|
||||
x => StatescriptEditorControls.GetVectorComponent(_currentValue, _valueType, x),
|
||||
OnVectorValueChanged);
|
||||
}
|
||||
|
||||
return new Label { Text = _valueType.ToString() };
|
||||
}
|
||||
|
||||
private void OnBoolValueChanged(bool x)
|
||||
{
|
||||
_currentValue = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnNumericValueChanged(double x)
|
||||
{
|
||||
_currentValue = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void OnVectorValueChanged(double[] x)
|
||||
{
|
||||
_currentValue = StatescriptEditorControls.BuildVectorVariant(_valueType, x);
|
||||
_onChanged?.Invoke();
|
||||
}
|
||||
|
||||
private VBoxContainer CreateArrayEditor()
|
||||
{
|
||||
var vBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
|
||||
_elementsContainer = new VBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
Visible = _isArrayExpanded,
|
||||
};
|
||||
|
||||
var headerRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
vBox.AddChild(headerRow);
|
||||
|
||||
_toggleButton = new Button
|
||||
{
|
||||
Text = $"Array (size {_arrayValues.Count})",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
ToggleMode = true,
|
||||
ButtonPressed = _isArrayExpanded,
|
||||
};
|
||||
|
||||
_toggleButton.Toggled += OnArrayToggled;
|
||||
|
||||
headerRow.AddChild(_toggleButton);
|
||||
|
||||
Texture2D addIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Add", "EditorIcons");
|
||||
|
||||
var addButton = new Button
|
||||
{
|
||||
Icon = addIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Add Element",
|
||||
CustomMinimumSize = new GodotVector2(24, 24),
|
||||
};
|
||||
|
||||
addButton.Pressed += OnAddElementPressed;
|
||||
|
||||
headerRow.AddChild(addButton);
|
||||
|
||||
vBox.AddChild(_elementsContainer);
|
||||
|
||||
RebuildArrayElements();
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private void OnArrayToggled(bool toggled)
|
||||
{
|
||||
if (_elementsContainer is not null)
|
||||
{
|
||||
_elementsContainer.Visible = toggled;
|
||||
}
|
||||
|
||||
_isArrayExpanded = toggled;
|
||||
_onChanged?.Invoke();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private void OnAddElementPressed()
|
||||
{
|
||||
GodotVariant defaultValue = StatescriptVariableTypeConverter.CreateDefaultGodotVariant(_valueType);
|
||||
_arrayValues.Add(defaultValue);
|
||||
_onChanged?.Invoke();
|
||||
|
||||
if (_elementsContainer is not null)
|
||||
{
|
||||
_elementsContainer.Visible = true;
|
||||
}
|
||||
|
||||
_isArrayExpanded = true;
|
||||
RebuildArrayElements();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
private void RebuildArrayElements()
|
||||
{
|
||||
if (_elementsContainer is null || _toggleButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Node child in _elementsContainer.GetChildren())
|
||||
{
|
||||
_elementsContainer.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
|
||||
_toggleButton.Text = $"Array (size {_arrayValues.Count})";
|
||||
|
||||
Texture2D removeIcon = EditorInterface.Singleton.GetEditorTheme().GetIcon("Remove", "EditorIcons");
|
||||
|
||||
for (var i = 0; i < _arrayValues.Count; i++)
|
||||
{
|
||||
var capturedIndex = i;
|
||||
|
||||
if (StatescriptEditorControls.IsVectorType(_valueType))
|
||||
{
|
||||
var elementVBox = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_elementsContainer.AddChild(elementVBox);
|
||||
|
||||
var labelRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
elementVBox.AddChild(labelRow);
|
||||
labelRow.AddChild(new Label
|
||||
{
|
||||
Text = $"[{i}]",
|
||||
SizeFlagsHorizontal = SizeFlags.ExpandFill,
|
||||
});
|
||||
|
||||
AddArrayRemoveButton(labelRow, removeIcon, capturedIndex);
|
||||
|
||||
VBoxContainer vectorEditor = StatescriptEditorControls.CreateVectorEditor(
|
||||
_valueType,
|
||||
x =>
|
||||
{
|
||||
return StatescriptEditorControls.GetVectorComponent(
|
||||
_arrayValues[capturedIndex],
|
||||
_valueType,
|
||||
x);
|
||||
},
|
||||
x =>
|
||||
{
|
||||
_arrayValues[capturedIndex] =
|
||||
StatescriptEditorControls.BuildVectorVariant(_valueType, x);
|
||||
_onChanged?.Invoke();
|
||||
});
|
||||
|
||||
elementVBox.AddChild(vectorEditor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var elementRow = new HBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||
_elementsContainer.AddChild(elementRow);
|
||||
elementRow.AddChild(new Label { Text = $"[{i}]" });
|
||||
|
||||
if (_valueType == StatescriptVariableType.Bool)
|
||||
{
|
||||
elementRow.AddChild(StatescriptEditorControls.CreateBoolEditor(
|
||||
_arrayValues[capturedIndex].AsBool(),
|
||||
x =>
|
||||
{
|
||||
_arrayValues[capturedIndex] = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorSpinSlider spin = StatescriptEditorControls.CreateNumericSpinSlider(
|
||||
_valueType,
|
||||
_arrayValues[capturedIndex].AsDouble(),
|
||||
x =>
|
||||
{
|
||||
_arrayValues[capturedIndex] = GodotVariant.From(x);
|
||||
_onChanged?.Invoke();
|
||||
});
|
||||
|
||||
elementRow.AddChild(spin);
|
||||
}
|
||||
|
||||
AddArrayRemoveButton(elementRow, removeIcon, capturedIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddArrayRemoveButton(
|
||||
HBoxContainer row,
|
||||
Texture2D removeIcon,
|
||||
int elementIndex)
|
||||
{
|
||||
var removeButton = new Button
|
||||
{
|
||||
Icon = removeIcon,
|
||||
Flat = true,
|
||||
TooltipText = "Remove Element",
|
||||
CustomMinimumSize = new GodotVector2(24, 24),
|
||||
};
|
||||
|
||||
var handler = new ArrayRemoveHandler(this, elementIndex);
|
||||
removeButton.AddChild(handler);
|
||||
removeButton.Pressed += handler.HandlePressed;
|
||||
|
||||
row.AddChild(removeButton);
|
||||
}
|
||||
|
||||
private void OnRemoveElement(int elementIndex)
|
||||
{
|
||||
_arrayValues.RemoveAt(elementIndex);
|
||||
_onChanged?.Invoke();
|
||||
RebuildArrayElements();
|
||||
RaiseLayoutSizeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Godot-compatible signal handler for array element remove buttons. Holds the element index and a reference to the
|
||||
/// owning editor so the <c>Pressed</c> signal can be handled without a lambda.
|
||||
/// </summary>
|
||||
[Tool]
|
||||
private sealed partial class ArrayRemoveHandler : Node
|
||||
{
|
||||
private readonly VariantResolverEditor _editor;
|
||||
private readonly int _elementIndex;
|
||||
|
||||
public ArrayRemoveHandler()
|
||||
{
|
||||
_editor = null!;
|
||||
}
|
||||
|
||||
public ArrayRemoveHandler(VariantResolverEditor editor, int elementIndex)
|
||||
{
|
||||
_editor = editor;
|
||||
_elementIndex = elementIndex;
|
||||
}
|
||||
|
||||
public void HandlePressed()
|
||||
{
|
||||
_editor.OnRemoveElement(_elementIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv2fk6v67mt3u
|
||||
Reference in New Issue
Block a user