A placed prop component in a roleplay gamemode. Syncs owner, model source, label and flags from the host, loads the model per machine and sets up either a model or box collider plus a rigidbody, and exposes an E-press interaction when the prop is the bank ATM model.
namespace HorizonRP;
/// <summary>
/// Un prop posé par un joueur. Créé et possédé par l'hôte (PropSystem), qui seul
/// en écrit les valeurs ; chaque machine charge le modèle elle-même. Le prop a la
/// physique de base (corps simulé par l'hôte), sauf figé au physgun. À la déconnexion de son propriétaire, un prop venu
/// de ses poches y retourne ; le matériel de service et ceux du staff disparaissent.
/// </summary>
[Title( "Horizon RP – Prop posé" )]
[Icon( "chair" )]
public sealed class PlacedProp : Component, Component.IPressable
{
[Sync( SyncFlags.FromHost )] public ulong OwnerId { get; set; }
[Sync( SyncFlags.FromHost )] public string OwnerName { get; set; }
[Sync( SyncFlags.FromHost )] public long EntryId { get; set; }
[Sync( SyncFlags.FromHost )] public string ModelSource { get; set; }
[Sync( SyncFlags.FromHost )] public string Label { get; set; }
/// <summary>Posé depuis les poches : ramassé, il y retourne.</summary>
[Sync( SyncFlags.FromHost )] public bool FromItem { get; set; }
/// <summary>Matériel de service : rangé quand son propriétaire quitte le métier.</summary>
[Sync( SyncFlags.FromHost )] public bool Service { get; set; }
/// <summary>Hôte : l'ordre de pose, pour « annuler le dernier » (touche Z).</summary>
public int Order { get; set; }
public static IEnumerable<PlacedProp> All =>
Game.ActiveScene?.GetAllComponents<PlacedProp>() ?? Enumerable.Empty<PlacedProp>();
/// <summary>
/// Le prop « Distributeur HZ Bank » posé est un vrai distributeur, comme celui de la ville
/// (Julien, 30/10/2026 : la touche E ne proposait rien). Il est réservé au staff au catalogue.
/// </summary>
public bool IsAtm => ModelSource == Atm.ModelPath;
// Touche E : l'écran du distributeur (UI/AtmScreen.razor) ; l'hôte le compte dans Atm.All.
public bool CanPress( IPressable.Event e ) => IsAtm && Atm.InPressReach( GameObject );
public bool Press( IPressable.Event e )
{
if ( !IsAtm )
return false;
RoleplayPlayer.Local?.RequestAtmOpen( GameObject.Id );
return true;
}
public IPressable.Tooltip? GetTooltip( IPressable.Event e ) =>
IsAtm && Atm.InPressReach( GameObject ) ? new IPressable.Tooltip( "Distributeur HZ Bank", "local_atm", "Retrait, dépôt, solde" ) : null;
string appliedSource;
protected override void OnUpdate()
{
if ( appliedSource == ModelSource )
return;
appliedSource = ModelSource;
_ = ApplyModel( ModelSource );
}
async Task ApplyModel( string source )
{
var model = await ModelSources.Load( source );
if ( !IsValid || source != ModelSource )
return;
var renderer = GetOrAddComponent<ModelRenderer>();
renderer.Model = model;
// La physique de base (Julien, 30/10/2026) : un prop tombe, se renverse, se pousse, sauf
// s'il a été figé au physgun (clic droit, AdminActions.FrozenTag). Il était en collision
// statique : lâché en l'air au physgun, il y restait. Un corps dynamique veut des formes
// convexes ; un modèle qui n'en a pas (box, mannequin…) reçoit une boîte à sa taille.
var convex = model?.Physics?.Parts?.Any( p => p.Hulls.Any() || p.Spheres.Any() || p.Capsules.Any() ) == true;
if ( convex )
{
Components.Get<BoxCollider>()?.Destroy();
var collider = GetOrAddComponent<ModelCollider>();
collider.Model = model;
collider.Static = false;
}
else
{
Components.Get<ModelCollider>()?.Destroy();
var box = GetOrAddComponent<BoxCollider>();
box.Center = model?.Bounds.Center ?? Vector3.Up * 8;
box.Scale = model?.Bounds.Size ?? 16;
}
// Simulée par l'hôte, à qui le prop appartient ; les autres reçoivent sa position.
var body = GetOrAddComponent<Rigidbody>();
body.MotionEnabled = !IsProxy && !GameObject.Tags.Has( AdminActions.FrozenTag );
}
}