Inventory/DroppedItem.cs
namespace HorizonRP;

/// <summary>
/// Un objet jeté au sol. Créé par l'hôte ; n'importe qui le ramasse avec E.
/// Disparaît au bout de 10 minutes, pour que la ville ne se remplisse pas de
/// canettes.
/// </summary>
[Title( "Horizon RP – Objet au sol" )]
[Icon( "inventory_2" )]
public sealed class DroppedItem : Component, Component.IPressable
{
	public const float Lifetime = 600f;

	[Sync( SyncFlags.FromHost )] public string ItemId { get; set; }
	[Sync( SyncFlags.FromHost )] public int Count { get; set; }

	/// <summary>Hôte : ce que l'objet garde avec lui (une arme jetée garde sa série et ses balles).</summary>
	public string Data { get; set; }

	public ItemDefinition Definition => Items.Get( ItemId );

	public static IEnumerable<DroppedItem> All =>
		Game.ActiveScene?.GetAllComponents<DroppedItem>() ?? Enumerable.Empty<DroppedItem>();

	RealTimeSince sinceSpawn;
	string appliedModel;

	/// <summary>Hôte : pose un objet au sol.</summary>
	public static void Spawn( ItemDefinition item, int count, Vector3 position, string data = null )
	{
		var go = new GameObject( true, $"Au sol : {item.Name}" );
		go.WorldPosition = position + Vector3.Up * 2;
		go.WorldRotation = Rotation.FromYaw( Random.Shared.Float( 0, 360 ) );

		var dropped = go.Components.Create<DroppedItem>();
		dropped.ItemId = item.Id;
		dropped.Count = count;
		dropped.Data = data;

		go.NetworkSpawn();
	}

	// Sans cette remise à zéro, RealTimeSince compterait depuis le lancement du
	// jeu et l'objet disparaîtrait aussitôt posé.
	protected override void OnStart() => sinceSpawn = 0;

	protected override void OnUpdate()
	{
		var model = Definition?.WorldModel;
		if ( model != appliedModel )
		{
			appliedModel = model;
			_ = ApplyModel( model );
		}

		if ( Networking.IsHost && sinceSpawn > Lifetime )
			GameObject.Destroy();
	}

	async Task ApplyModel( string source )
	{
		var model = await ModelSources.Load( source );
		if ( !IsValid )
			return;

		// Le modèle vit dans un enfant local : on peut le coucher sans toucher à la
		// position de l'objet, que l'hôte tient en réseau.
		var holder = GameObject.Children.FirstOrDefault( x => x.Name == "Modèle" ) ?? new GameObject( GameObject, true, "Modèle" );
		holder.NetworkMode = NetworkMode.Never;
		holder.LocalRotation = Rotation.Identity;
		holder.LocalPosition = Vector3.Zero;
		// Un modèle venu d'un autre jeu garde au sol la taille qu'il a en main.
		var scale = WeaponRules.GripOf( model.ResourcePath )?.Scale ?? 1f;
		holder.LocalScale = scale;

		// Une arme est faite pour la main : on la couche sur son côté le plus plat.
		if ( WeaponItems.WeaponOf( ItemId ) is not null )
			LayFlat( holder, new BBox( model.Bounds.Mins * scale, model.Bounds.Maxs * scale ) );

		holder.GetOrAddComponent<ModelRenderer>().Model = model;

		// Un collider « déclencheur » : on peut viser l'objet sans buter dedans.
		var collider = holder.GetOrAddComponent<ModelCollider>();
		collider.Model = model;
		collider.IsTrigger = true;
	}

	/// <summary>
	/// Couche le modèle : parmi les quarts de tour, celui qui le rend le moins haut, posé
	/// de sorte que son point le plus bas touche le sol.
	/// </summary>
	static void LayFlat( GameObject holder, BBox bounds )
	{
		var best = Rotation.Identity;
		var bestHeight = float.MaxValue;
		var bestBottom = 0f;
		foreach ( var pitch in new[] { 0f, 90f, 180f, 270f } )
		{
			foreach ( var roll in new[] { 0f, 90f, 180f, 270f } )
			{
				var rotation = Rotation.From( pitch, 0, roll );
				var corners = bounds.Corners.Select( c => rotation * c ).ToList();
				var bottom = corners.Min( c => c.z );
				var height = corners.Max( c => c.z ) - bottom;
				if ( height < bestHeight - 0.01f )
				{
					best = rotation;
					bestHeight = height;
					bestBottom = bottom;
				}
			}
		}

		holder.LocalRotation = best;
		holder.LocalPosition = Vector3.Up * -bestBottom;
	}

	public bool Press( IPressable.Event e )
	{
		InventoryActions.RequestPickup( GameObject.Id );
		return true;
	}

	public IPressable.Tooltip? GetTooltip( IPressable.Event e ) =>
		Definition is { } item ? new IPressable.Tooltip( Count > 1 ? $"{item.Name} ×{Count}" : item.Name, item.Icon, "Ramasser" ) : null;
}