Actors/Actor.Mechanics.cs
namespace Opium;

public partial class Actor
{
	/// <summary>
	/// Maintains a list of mechanics that are associated with this player controller.
	/// </summary>
	public IEnumerable<ActorMechanic> Mechanics => Components.GetAll<ActorMechanic>( FindMode.EnabledInSelfAndDescendants ).OrderBy( x => x.Priority );

	protected float? CurrentSpeedOverride;
	protected float? CurrentEyeHeightOverride;
	protected float? CurrentFrictionOverride;
	protected float? CurrentAccelerationOverride;
	protected bool LockMovementOverride;
	protected bool LockMouseMovementOverride;

	ActorMechanic[] ActiveMechanics = { };

	public ITagSet MechanicTags { get; protected set; } = new TagSet();

	/// <summary>
	/// Is a mechanic active?
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <returns></returns>
	public bool IsMechanicActive<T>() where T : ActorMechanic
	{
		return ActiveMechanics.OfType<T>()
			.Any( x => x.Active );
	}

	/// <summary>
	/// Gets a mechanic of the specified type.
	/// </summary>
	/// <typeparam name="T"></typeparam>
	/// <returns></returns>
	public T GetMechanic<T>() where T : ActorMechanic
	{
		return Mechanics.OfType<T>().FirstOrDefault();
	}

	/// <summary>
	/// Called on <see cref="OnUpdate"/>.
	/// </summary>
	protected virtual void DoMechanicsUpdate()
	{
		var lastUpdate = ActiveMechanics;
		var sortedMechanics = Mechanics.Where( x => x.ShouldBecomeActive() || !x.ShouldBecomeInactive() );

		// Copy the previous update's tags so we can compare / send tag changed events later.
		var previousUpdateTags = MechanicTags;

		// Clear the current tags
		var currentTags = new TagSet();

		float? speedOverride = null;
		float? eyeHeightOverride = null;
		float? frictionOverride = null;
		float? accelerationOverride = null;
		bool lockMovementOverride = false;
		bool lockMouseOverride = false;

		foreach ( var mechanic in sortedMechanics )
		{
			mechanic.IsActive = true;
			mechanic.OnActiveUpdate();

			// Add tags where we can
			mechanic.GetTags()
				.ToList()
				.ForEach( currentTags.Add );

			var eyeHeight = mechanic.GetEyeHeight();
			var speed = mechanic.GetSpeed();
			var friction = mechanic.GetGroundFriction();
			var acceleration = mechanic.GetAcceleration();
			var lockMovement = mechanic.LockMovement;
			var lockMouse = mechanic.LockMouseMovement;

			mechanic.BuildWishInput( ref WishMove );
			if ( speed is not null ) speedOverride = speed;
			if ( eyeHeight is not null ) eyeHeightOverride = eyeHeight;
			if ( friction is not null ) frictionOverride = friction;
			if ( acceleration is not null ) accelerationOverride = acceleration;

			if ( lockMovement ) lockMovementOverride = true;
			if ( lockMouse ) lockMouseOverride = true;
		}

		ActiveMechanics = sortedMechanics.ToArray();

		if ( lastUpdate is not null )
		{
			foreach ( var mechanic in lastUpdate?.Except( sortedMechanics ) )
			{
				// This mechanic shouldn't be active anymore
				mechanic.IsActive = false;
			}
		}

		CurrentSpeedOverride = speedOverride;
		CurrentEyeHeightOverride = eyeHeightOverride;
		CurrentFrictionOverride = frictionOverride;
		CurrentAccelerationOverride = accelerationOverride;
		LockMovementOverride = lockMovementOverride;
		LockMouseMovementOverride = lockMouseOverride;

		MechanicTags.SetFrom( currentTags );
	}

	/// <summary>
	/// Give a mechanic to the actor from a prefab.
	/// </summary>
	/// <param name="prefab"></param>
	/// <param name="lifetime"></param>
	public ActorMechanic GiveMechanicFromPrefab( GameObject prefab, float lifetime = 0f )
	{
		var instance = prefab.Clone();
		instance.SetParent( GameObject );

		var mechanic = instance.Components.Get<ActorMechanic>( FindMode.EverythingInSelfAndDescendants );
		Log.Info( $"Added runtime mechanic: {mechanic}" );

		mechanic.Actor = this;

		if ( lifetime > 0f )
		{
			instance.DestroyAsync( lifetime );
		}


		return mechanic;
	}
}