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;
}