RigControl/RigEvent.cs
using Sandbox;

namespace Marionette;

public enum RigEventSide
{
	Client,
	Server,
}

public sealed class RigEvent
{
	[Property] public string Name { get; set; } = "event";

	[Property, Group( "Timing" ), Title( "Start Frame" )] public int StartFrame { get; set; }
	[Property, Group( "Timing" ), Title( "End Frame" )] public int EndFrame { get; set; } = 1;

	// Which realm spawns the prop. Client-side is the common case for a cosmetic hold (nothing
	// else needs to know about it); Server needs to own anything that should hit, be networked,
	// or exist for other players.
	[Property, Group( "Attachment" )] public RigEventSide Side { get; set; } = RigEventSide.Client;

	[Property, Group( "Attachment" ), Title( "Model" )] public Model PropModel { get; set; }

	[Property, Group( "Attachment" ), Title( "Attach Bone" )] public string AttachBone { get; set; } = "hand_L";

	// Re-parent to the bone's live transform every frame rather than snapping once on attach --
	// off for a prop that's meant to be placed and left (e.g. set down mid-animation).
	[Property, Group( "Attachment" ), Title( "Follow Bone" )] public bool FollowBone { get; set; } = true;

	[Property, Group( "Offset" ), Title( "Position" )] public Vector3 PositionOffset { get; set; }
	[Property, Group( "Offset" ), Title( "Rotation" )] public Angles RotationOffset { get; set; }
	[Property, Group( "Offset" ), Title( "Scale" )] public float ScaleOffset { get; set; } = 1f;

	public bool IsActive( float frame ) => frame >= StartFrame && frame <= EndFrame;

	public Transform BoneOffsetTransform => new( PositionOffset, RotationOffset, ScaleOffset );
}