World/Hover.cs
using System;

namespace Causal;

public sealed class Hover : Component, Component.ExecuteInEditor
{
	[Property] public float HoverHeight { get; set; } = 8f;
	[Property] public float HoverSpeed { get; set; } = 1.5f;
	[Property] public float SpinSpeed { get; set; }

	private bool _hasBase;
	private Vector3 _basePosition;
	private Rotation _baseRotation;
	private Vector3 _lastWrittenPosition;
	private Rotation _lastWrittenRotation;
	private bool _hasLastWritten;

	protected override void OnUpdate()
	{
		float offset = MathF.Sin( Time.Now * HoverSpeed ) * HoverHeight;

		if ( !_hasBase || (_hasLastWritten && GameObject.WorldPosition != _lastWrittenPosition) )
		{
			_basePosition = GameObject.WorldPosition - Vector3.Up * offset;
			_baseRotation = GameObject.WorldRotation;
			_hasBase = true;
		}
		else if ( _hasLastWritten && GameObject.WorldRotation != _lastWrittenRotation )
		{
			_baseRotation = GameObject.WorldRotation * Rotation.FromYaw( -SpinSpeed * Time.Delta );
		}

		_lastWrittenPosition = _basePosition + Vector3.Up * offset;
		GameObject.WorldPosition = _lastWrittenPosition;

		if ( SpinSpeed != 0f )
		{
			GameObject.WorldRotation *= Rotation.FromYaw( SpinSpeed * Time.Delta );
		}

		_lastWrittenRotation = GameObject.WorldRotation;
		_hasLastWritten = true;
	}

	protected override void OnDisabled()
	{
		RestoreAuthoredTransform();
	}

	protected override void OnDestroy()
	{
		RestoreAuthoredTransform();
	}

	private void RestoreAuthoredTransform()
	{
		if ( !_hasBase )
		{
			return;
		}

		GameObject.WorldPosition = _basePosition;
		GameObject.WorldRotation = _baseRotation;
		_hasLastWritten = false;
	}
}