Files
MovementTests/addons/forge/editor/statescript/StatescriptEditorControls.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

422 lines
16 KiB
C#

// Copyright © Gamesmiths Guild.
#if TOOLS
using System;
using Gamesmiths.Forge.Godot.Resources.Statescript;
using Godot;
namespace Gamesmiths.Forge.Godot.Editor.Statescript;
/// <summary>
/// Shared factory methods for creating value-editor controls used by both the variable panel and resolver editors.
/// </summary>
internal static partial class StatescriptEditorControls
{
private static readonly Color _axisXColor = new(0.96f, 0.37f, 0.37f);
private static readonly Color _axisYColor = new(0.54f, 0.83f, 0.01f);
private static readonly Color _axisZColor = new(0.33f, 0.55f, 0.96f);
private static readonly Color _axisWColor = new(0.66f, 0.66f, 0.66f);
private static StyleBox? _cachedPanelStyle;
/// <summary>
/// Returns <see langword="true"/> for integer-like variable types.
/// </summary>
/// <param name="type">The variable type to check.</param>
public static bool IsIntegerType(StatescriptVariableType type)
{
return type is StatescriptVariableType.Int or StatescriptVariableType.UInt
or StatescriptVariableType.Long or StatescriptVariableType.ULong
or StatescriptVariableType.Short or StatescriptVariableType.UShort
or StatescriptVariableType.Byte or StatescriptVariableType.SByte
or StatescriptVariableType.Char;
}
/// <summary>
/// Returns <see langword="true"/> for floating-point variable types.
/// </summary>
/// <param name="type">The variable type to check.</param>
public static bool IsFloatType(StatescriptVariableType type)
{
return type is StatescriptVariableType.Float or StatescriptVariableType.Double
or StatescriptVariableType.Decimal;
}
/// <summary>
/// Returns <see langword="true"/> for multi-component vector/quaternion/plane variable types.
/// </summary>
/// <param name="type">The variable type to check.</param>
public static bool IsVectorType(StatescriptVariableType type)
{
return type is StatescriptVariableType.Vector2 or StatescriptVariableType.Vector3
or StatescriptVariableType.Vector4 or StatescriptVariableType.Plane
or StatescriptVariableType.Quaternion;
}
/// <summary>
/// Creates a <see cref="PanelContainer"/> wrapping a <see cref="CheckBox"/> for boolean editing.
/// </summary>
/// <param name="value">The initial value of the boolean.</param>
/// <param name="onChanged">An action invoked on value change.</param>
/// <returns>A <see cref="PanelContainer"/> containing a <see cref="CheckBox"/>.</returns>
public static PanelContainer CreateBoolEditor(bool value, Action<bool> onChanged)
{
var container = new PanelContainer
{
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
container.AddThemeStyleboxOverride("panel", GetPanelStyle());
var checkButton = new CheckBox
{
Text = "On",
ButtonPressed = value,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
};
var handler = new BoolSignalHandler { OnChanged = onChanged };
checkButton.AddChild(handler);
checkButton.Toggled += handler.HandleToggled;
container.AddChild(checkButton);
return container;
}
/// <summary>
/// Creates an <see cref="EditorSpinSlider"/> configured for the given numeric variable type.
/// </summary>
/// <param name="type">The type of the numeric variable.</param>
/// <param name="value">The initial value of the numeric variable.</param>
/// <param name="onChanged">An action invoked on value change.</param>
/// <returns>An <see cref="EditorSpinSlider"/> configured for the specified numeric variable type.</returns>
public static EditorSpinSlider CreateNumericSpinSlider(
StatescriptVariableType type,
double value,
Action<double>? onChanged = null)
{
NumericConfig config = GetNumericConfig(type);
var spin = new EditorSpinSlider
{
Step = config.Step,
Rounded = config.IsInteger,
EditingInteger = config.IsInteger,
MinValue = config.MinValue,
MaxValue = config.MaxValue,
AllowGreater = config.AllowBeyondRange,
AllowLesser = config.AllowBeyondRange,
ControlState = config.IsInteger
? EditorSpinSlider.ControlStateEnum.Default
: EditorSpinSlider.ControlStateEnum.Hide,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
Value = value,
};
var handler = new NumericSpinHandler(spin) { OnChanged = onChanged };
spin.AddChild(handler);
if (onChanged is not null)
{
spin.ValueChanged += handler.HandleValueChanged;
}
spin.Grabbed += handler.HandleGrabbed;
spin.Ungrabbed += handler.HandleUngrabbed;
spin.FocusExited += handler.HandleFocusExited;
return spin;
}
/// <summary>
/// Creates a panel with a row of labelled <see cref="EditorSpinSlider"/> controls for editing a vector value.
/// </summary>
/// <param name="type">The type of the vector/quaternion/plane.</param>
/// <param name="getComponent">A function to retrieve the value of a specific component.</param>
/// <param name="onChanged">An action to invoke when any component value changes.</param>
/// <returns>A <see cref="VBoxContainer"/> containing the vector editor controls.</returns>
public static VBoxContainer CreateVectorEditor(
StatescriptVariableType type,
Func<int, double> getComponent,
Action<double[]>? onChanged)
{
var componentCount = GetVectorComponentCount(type);
var labels = GetVectorComponentLabels(type);
var vBox = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
var row = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
row.AddThemeConstantOverride("separation", 0);
var panelContainer = new PanelContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill };
panelContainer.AddThemeStyleboxOverride("panel", GetPanelStyle());
vBox.AddChild(panelContainer);
panelContainer.AddChild(row);
var values = new double[componentCount];
var handler = new VectorComponentHandler(values) { OnChanged = onChanged };
vBox.AddChild(handler);
for (var i = 0; i < componentCount; i++)
{
values[i] = getComponent(i);
var spin = new EditorSpinSlider
{
Label = labels[i],
Step = 0.001,
Rounded = false,
EditingInteger = false,
AllowGreater = true,
AllowLesser = true,
Flat = false,
ControlState = EditorSpinSlider.ControlStateEnum.Hide,
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
SizeFlagsStretchRatio = 1,
CustomMinimumSize = new Vector2(71, 0),
Value = values[i],
};
spin.AddThemeColorOverride("label_color", GetComponentColor(i));
var componentHandler = new VectorSpinHandler(handler, i);
spin.AddChild(componentHandler);
spin.ValueChanged += componentHandler.HandleValueChanged;
spin.Grabbed += componentHandler.HandleGrabbed;
spin.Ungrabbed += componentHandler.HandleUngrabbed;
spin.FocusExited += componentHandler.HandleFocusExited;
row.AddChild(spin);
}
return vBox;
}
/// <summary>
/// Reads a single component from a vector/quaternion/plane variant.
/// </summary>
/// <param name="value">The variant containing the vector/quaternion/plane value.</param>
/// <param name="type">The type of the vector/quaternion/plane.</param>
/// <param name="index">The index of the component to retrieve.</param>
/// <exception cref="NotImplementedException">Exception thrown if the provided type is not a vector/quaternion/plane
/// type.</exception>
public static double GetVectorComponent(Variant value, StatescriptVariableType type, int index)
{
return type switch
{
StatescriptVariableType.Vector2 => index == 0
? value.AsVector2().X
: value.AsVector2().Y,
StatescriptVariableType.Vector3 => index switch
{
0 => value.AsVector3().X,
1 => value.AsVector3().Y,
_ => value.AsVector3().Z,
},
StatescriptVariableType.Vector4 => index switch
{
0 => value.AsVector4().X,
1 => value.AsVector4().Y,
2 => value.AsVector4().Z,
_ => value.AsVector4().W,
},
StatescriptVariableType.Plane => index switch
{
0 => value.AsPlane().Normal.X,
1 => value.AsPlane().Normal.Y,
2 => value.AsPlane().Normal.Z,
_ => value.AsPlane().D,
},
StatescriptVariableType.Quaternion => index switch
{
0 => value.AsQuaternion().X,
1 => value.AsQuaternion().Y,
2 => value.AsQuaternion().Z,
_ => value.AsQuaternion().W,
},
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => 0,
};
}
/// <summary>
/// Builds a Godot <see cref="Variant"/> from a component array for the given vector/quaternion/plane type.
/// </summary>
/// <param name="type">The type of the vector/quaternion/plane.</param>
/// <param name="values">The array of component values.</param>
/// <returns>A <see cref="Variant"/> representing the vector/quaternion/plane.</returns>
/// <exception cref="NotImplementedException">Exception thrown if the provided type is not a vector/quaternion/plane
/// type.</exception>
public static Variant BuildVectorVariant(StatescriptVariableType type, double[] values)
{
return type switch
{
StatescriptVariableType.Vector2 => Variant.From(
new Vector2((float)values[0], (float)values[1])),
StatescriptVariableType.Vector3 => Variant.From(
new Vector3(
(float)values[0],
(float)values[1],
(float)values[2])),
StatescriptVariableType.Vector4 => Variant.From(
new Vector4(
(float)values[0],
(float)values[1],
(float)values[2],
(float)values[3])),
StatescriptVariableType.Plane => Variant.From(
new Plane(
new Vector3(
(float)values[0],
(float)values[1],
(float)values[2]),
(float)values[3])),
StatescriptVariableType.Quaternion => Variant.From(
new Quaternion(
(float)values[0],
(float)values[1],
(float)values[2],
(float)values[3])),
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => Variant.From(0),
};
}
private static int GetVectorComponentCount(StatescriptVariableType type)
{
return type switch
{
StatescriptVariableType.Vector2 => 2,
StatescriptVariableType.Vector3 => 3,
StatescriptVariableType.Vector4 => 4,
StatescriptVariableType.Plane => 4,
StatescriptVariableType.Quaternion => 4,
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => 4,
};
}
private static string[] GetVectorComponentLabels(StatescriptVariableType type)
{
return type switch
{
StatescriptVariableType.Vector2 => ["x", "y"],
StatescriptVariableType.Vector3 => ["x", "y", "z"],
StatescriptVariableType.Plane => ["x", "y", "z", "d"],
StatescriptVariableType.Vector4 => ["x", "y", "z", "w"],
StatescriptVariableType.Quaternion => ["x", "y", "z", "w"],
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Byte => throw new NotImplementedException(),
StatescriptVariableType.SByte => throw new NotImplementedException(),
StatescriptVariableType.Char => throw new NotImplementedException(),
StatescriptVariableType.Decimal => throw new NotImplementedException(),
StatescriptVariableType.Double => throw new NotImplementedException(),
StatescriptVariableType.Float => throw new NotImplementedException(),
StatescriptVariableType.Int => throw new NotImplementedException(),
StatescriptVariableType.UInt => throw new NotImplementedException(),
StatescriptVariableType.Long => throw new NotImplementedException(),
StatescriptVariableType.ULong => throw new NotImplementedException(),
StatescriptVariableType.Short => throw new NotImplementedException(),
StatescriptVariableType.UShort => throw new NotImplementedException(),
_ => ["x", "y", "z", "w"],
};
}
private static Color GetComponentColor(int index)
{
return index switch
{
0 => _axisXColor,
1 => _axisYColor,
2 => _axisZColor,
_ => _axisWColor,
};
}
private static NumericConfig GetNumericConfig(StatescriptVariableType type)
{
return type switch
{
StatescriptVariableType.Byte => new NumericConfig(byte.MinValue, byte.MaxValue, 1, true, false),
StatescriptVariableType.SByte => new NumericConfig(sbyte.MinValue, sbyte.MaxValue, 1, true, false),
StatescriptVariableType.Char => new NumericConfig(char.MinValue, char.MaxValue, 1, true, false),
StatescriptVariableType.Short => new NumericConfig(short.MinValue, short.MaxValue, 1, true, false),
StatescriptVariableType.UShort => new NumericConfig(ushort.MinValue, ushort.MaxValue, 1, true, false),
StatescriptVariableType.Int => new NumericConfig(int.MinValue, int.MaxValue, 1, true, false),
StatescriptVariableType.UInt => new NumericConfig(uint.MinValue, uint.MaxValue, 1, true, false),
// Godot's interface starts acting weird if we try to use the full range of long/ulong, so we clamp to +/-
// 9e18 which should be sufficient for most use cases.
StatescriptVariableType.Long => new NumericConfig(-9e18, 9e18, 1, true, false),
StatescriptVariableType.ULong => new NumericConfig(0, 9e18, 1, true, false),
StatescriptVariableType.Float => new NumericConfig(-1e10, 1e10, 0.001, false, true),
StatescriptVariableType.Double => new NumericConfig(-1e10, 1e10, 0.001, false, true),
StatescriptVariableType.Decimal => new NumericConfig(-1e10, 1e10, 0.001, false, true),
StatescriptVariableType.Bool => throw new NotImplementedException(),
StatescriptVariableType.Vector2 => throw new NotImplementedException(),
StatescriptVariableType.Vector3 => throw new NotImplementedException(),
StatescriptVariableType.Vector4 => throw new NotImplementedException(),
StatescriptVariableType.Plane => throw new NotImplementedException(),
StatescriptVariableType.Quaternion => throw new NotImplementedException(),
_ => new NumericConfig(-1e10, 1e10, 0.001, false, true),
};
}
private readonly record struct NumericConfig(
double MinValue,
double MaxValue,
double Step,
bool IsInteger,
bool AllowBeyondRange);
private static StyleBox GetPanelStyle()
{
if (_cachedPanelStyle is null || !GodotObject.IsInstanceValid(_cachedPanelStyle))
{
Control baseControl = EditorInterface.Singleton.GetBaseControl();
_cachedPanelStyle = (StyleBox)baseControl.GetThemeStylebox("normal", "LineEdit").Duplicate();
_cachedPanelStyle.SetContentMarginAll(0);
}
return _cachedPanelStyle;
}
}
#endif