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.
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" );
}
}