City/CityObject.cs

A city object placed by staff, synced from the host with its kind, label and model source. Each client loads the model itself and the component handles the E press for ATMs, job NPCs and garages.

Networking
namespace HorizonRP;

/// <summary>
/// Un objet posé par le staff. Créé et possédé par l'hôte (CityManager), qui
/// seul en écrit les valeurs ; chaque machine charge le modèle elle-même à
/// partir de <see cref="ModelSource"/>.
///
/// Les distributeurs et les PNJ de métier réagissent à la touche E : c'est le
/// PlayerController du joueur qui appelle <see cref="Press"/>, chez lui.
/// </summary>
[Title( "Horizon RP – Objet de la ville" )]
[Icon( "location_on" )]
public sealed class CityObject : Component, Component.IPressable
{
	[Sync( SyncFlags.FromHost )] public long PlacementId { get; set; }
	[Sync( SyncFlags.FromHost )] public string Kind { get; set; } = CityKinds.Decor;
	[Sync( SyncFlags.FromHost )] public string ModelSource { get; set; }
	[Sync( SyncFlags.FromHost )] public string Label { get; set; }

	/// <summary>Donnée propre à la sorte : l'Id du métier pour un PNJ.</summary>
	[Sync( SyncFlags.FromHost )] public string Data { get; set; }

	public CityKind KindInfo => CityKinds.Get( Kind );

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

	string appliedSource;
	ModelRenderer renderer;
	ModelCollider collider;

	protected override void OnUpdate()
	{
		// Pas de rappel « Change » : un simple relevé suffit et reste juste même
		// quand la valeur arrive avec l'instantané de connexion.
		if ( appliedSource != ModelSource )
		{
			appliedSource = ModelSource;
			_ = ApplyModel( ModelSource );
		}

		if ( renderer.IsValid() )
		{
			// Un point d'apparition n'existe que pour le staff.
			renderer.Enabled = Kind != CityKinds.Spawn || (RoleplayPlayer.Local?.IsStaff ?? false);
		}
	}

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

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

		// Un point d'apparition ne doit pas bloquer les joueurs qui y arrivent.
		if ( Kind == CityKinds.Spawn )
			return;

		collider = GetOrAddComponent<ModelCollider>();
		collider.Model = model;
		collider.Static = true;
	}

	// Touche E

	public bool CanPress( IPressable.Event e ) => Kind is CityKinds.JobNpc or CityKinds.Garage || Kind == CityKinds.Atm && Atm.InPressReach( GameObject );

	public bool Press( IPressable.Event e )
	{
		if ( Kind == CityKinds.Garage )
		{
			Scene.GetAllComponents<GarageMenu>().FirstOrDefault()?.Open();
			return true;
		}

		// Le distributeur : l'hôte le donne s'il est libre, puis la vue se pose devant son écran
		// (RoleplayPlayer.RequestAtmOpen, UI/AtmScreen.razor).
		if ( Kind == CityKinds.Atm )
		{
			RoleplayPlayer.Local?.RequestAtmOpen( GameObject.Id );
			return true;
		}

		var hud = Scene.GetAllComponents<HorizonHud>().FirstOrDefault();
		if ( hud is null )
			return false;

		if ( Kind == CityKinds.JobNpc )
			hud.OpenTab( "metiers" );

		return true;
	}

	public IPressable.Tooltip? GetTooltip( IPressable.Event e ) => Kind switch
	{
		CityKinds.Atm => Atm.InPressReach( GameObject ) ? new IPressable.Tooltip( Label ?? "Distributeur HZ Bank", "local_atm", "Retrait, dépôt, solde" ) : null,
		CityKinds.JobNpc => new IPressable.Tooltip( Label ?? "Pôle emploi", "badge", "Choisir un métier" ),
		CityKinds.Garage => new IPressable.Tooltip( Label ?? "Garage", "garage", "Concession et garage" ),
		_ => null,
	};
}