Files
MovementTests/addons/forge/editor/statescript/resolvers/VariantResolverEditor.cs
Minimata 1d856fd937
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 26s
Create tag and build when new code gets to main / Export (push) Successful in 5m42s
Replicated the weapon flying tick setup using resources
2026-04-07 16:32:26 +02:00

389 lines
8.9 KiB
C#

// 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