Ems/RoleplayPlayer.Ems.cs

Partial RoleplayPlayer class holding the EMS (paramedic) state for a player: on-duty flag, grade, badge and callsign synced from the host, plus collider suppression logic that hides the player's capsules when seated, ragdolled or just hit by a vehicle. Also contains EmsInputSystem, which clears the use key for a downed player riding in a vehicle.

Networking
namespace HorizonRP;

/// <summary>
/// L'état « secours » d'un joueur, vu de tous (écrit par l'hôte, Police/PoliceService.Ems.cs) :
/// inscrit aux effectifs de l'UCFD (grade, matricule, indicatif), en service ou non. Séparé de
/// celui de la police (OnDuty, PoliceGrade) : un même joueur peut figurer aux deux effectifs, son
/// métier dit lequel il exerce.
/// </summary>
public sealed partial class RoleplayPlayer
{
	/// <summary>Secouriste en service (vestiaire de l'hôpital) : seul cas où ses droits de secours s'appliquent.</summary>
	[Sync( SyncFlags.FromHost )] public bool EmsOnDuty { get; set; }

	/// <summary>Le grade aux secours (EmsRules.Grades), -1 s'il n'est pas inscrit.</summary>
	[Sync( SyncFlags.FromHost )] public int EmsGrade { get; set; } = -1;

	[Sync( SyncFlags.FromHost )] public string EmsBadge { get; set; }
	[Sync( SyncFlags.FromHost )] public string EmsCallsign { get; set; }

	/// <summary>Un secouriste de l'UCFD en service.</summary>
	public bool IsEms => EmsOnDuty && JobId == EmsRules.JobId && EmsGrade >= 0;

	/// <summary>Assis dans un véhicule (ou sur un siège).</summary>
	public bool Seated => GameObject.GetComponentInParent<BaseChair>() is not null;

	// Ce que UpdateCapsules a éteint, pour le rallumer quand plus rien ne l'exige.
	readonly List<Collider> seatHiddenColliders = new();
	GameObject hiddenCapsules;
	Rigidbody seatHiddenBody;
	RealTimeSince sinceRunThrough = 100;

	/// <summary>Le temps qu'un véhicule qui fauche ce joueur passe à travers sa capsule (s).</summary>
	public const float RunThroughSeconds = 1.5f;

	/// <summary>
	/// Sur la machine qui simule un véhicule (Vehicle.CheckPedestrians) : il vient de faucher ce
	/// joueur. Sa capsule s'efface un instant ici, sinon la voiture pilait net dessus : chez elle, la
	/// capsule d'un autre joueur est un mur immobile, et l'hôte ne l'a pas encore renversé (un
	/// aller-retour réseau sur le dédié). Julien, 30/10/2026.
	/// </summary>
	public void LetVehicleThrough() => sinceRunThrough = 0;

	/// <summary>
	/// Sur toutes les machines, à chaque image : les capsules du joueur (l'objet ColliderObject du
	/// PlayerController — elles sont sur un enfant, jamais sur la racine) s'éteignent quand :
	/// - il est assis : le siège ne les éteint que chez lui ; chez le conducteur, qui simule la
	///   voiture, celle d'un passager restait dans la carrosserie et la voiture sautait en l'air ;
	/// - il est au sol en ragdoll (inconscient, renversé) : elles restaient au point du choc, un mur
	///   invisible même quand le corps était projeté à vingt mètres ;
	/// - un véhicule vient de le faucher (LetVehicleThrough).
	/// Rallumées quand plus rien ne l'exige ; chez lui assis, le siège les a éteintes lui-même et les
	/// rallumera. Appelé après UpdateDowned, qui décide du ragdoll.
	/// </summary>
	void UpdateCapsules()
	{
		var controller = GetComponent<PlayerController>();
		var seated = Seated;
		var off = seated || ragdollShown || sinceRunThrough < RunThroughSeconds;
		if ( off )
		{
			if ( controller?.ColliderObject is { Enabled: true } capsules )
			{
				capsules.Enabled = false;
				hiddenCapsules = capsules;
			}
		}
		else if ( hiddenCapsules.IsValid() )
		{
			hiddenCapsules.Enabled = true;
			hiddenCapsules = null;
		}

		// Assis : le corps physique aussi, comme le fait le siège chez lui.
		if ( seated )
		{
			if ( controller?.Body is { Enabled: true } body )
			{
				body.Enabled = false;
				seatHiddenBody = body;
			}
			// Une capsule de la racine rallumée par le ragdoll d'un inconscient installé par les
			// secours (RoleplayPlayer.Death.cs : StopRagdoll) reste éteinte.
			foreach ( var c in GameObject.Components.GetAll<Collider>( FindMode.EnabledInSelf ) )
			{
				c.Enabled = false;
				seatHiddenColliders.Add( c );
			}
			return;
		}
		if ( seatHiddenBody.IsValid() )
			seatHiddenBody.Enabled = true;
		seatHiddenBody = null;
		foreach ( var c in seatHiddenColliders )
			if ( c.IsValid() )
				c.Enabled = true;
		seatHiddenColliders.Clear();
	}

	// Réponses de l'hôte, chez le secouriste ou le patient (UI/PoliceMenu.razor, vue « Secours »)

	/// <summary>Chez le secouriste : le diagnostic d'un patient (texte prêt à afficher, lignes séparées par « \n »).</summary>
	[Rpc.Owner( NetFlags.HostOnly )]
	public void EmsDiagnosis( Guid patient, string text ) => PoliceMenu.ReceiveDiagnosis( patient, text );

	/// <summary>Chez le patient : ses factures de soins impayées, lues à l'accueil de l'hôpital (JSON), ou l'erreur.</summary>
	[Rpc.Owner( NetFlags.HostOnly )]
	public void EmsDeskBills( string json, string error, int deskPrice, bool medicsOnDuty ) => PoliceMenu.ReceiveEmsDesk( json, error, deskPrice, medicsOnDuty );
}

/// <summary>
/// Un inconscient installé dans un véhicule (les secours le transportent) ne descend pas seul : E
/// coupé au début de l'image, après nos touches (KeybindSystem, -1000) et comme les menottes (-900).
/// </summary>
public sealed class EmsInputSystem : GameObjectSystem
{
	public EmsInputSystem( Scene scene ) : base( scene )
	{
		Listen( Stage.StartUpdate, -890, Frame, "Patient Horizon RP" );
		Listen( Stage.StartFixedUpdate, -890, Frame, "Patient Horizon RP (fixe)" );
	}

	void Frame()
	{
		if ( Application.IsDedicatedServer || !Game.IsPlaying )
			return;
		if ( RoleplayPlayer.Local is { Downed: true } me && me.Seated )
			Input.Clear( "use" );
	}
}