A Voice component for Horizon RP, placed on the player prefab. Chooses between proximity voice and phone call audio: the listener machine switches the voice to non-positional when in a call, and the speaker's own machine keeps the mic open during calls or while holding the voice key.
namespace HorizonRP;
/// <summary>
/// La voix d'un joueur, posée sur son prefab à la place du composant Voice.
///
/// Le moteur n'enregistre qu'une voix à la fois par joueur : la proximité et le
/// téléphone sont donc UNE seule voix, dont on change le rendu.
/// - Sur la machine de l'auditeur : si l'auditeur est en appel avec ce joueur,
/// la voix est jouée « dans les oreilles » (sans position 3D) ; sinon, en 3D,
/// comme une voix normale qu'on n'entend que de près.
/// - Sur la machine du joueur lui-même : en appel, le micro reste ouvert (comme
/// un vrai téléphone) ; sinon, on parle en maintenant la touche V.
/// </summary>
[Title( "Horizon RP – Voix" )]
[Icon( "record_voice_over" )]
public sealed class RpVoice : Voice
{
/// <summary>Portée de la voix de proximité (unités s&box, ~25 m).</summary>
const float ProximityRange = 1000f;
protected override void OnFixedUpdate()
{
var speaker = GetComponent<RoleplayPlayer>();
var phone = PhoneSystem.Current;
if ( speaker is null || phone is null )
return;
if ( !IsProxy )
{
// L'écoute est décidée ici (mode Manuel) plutôt que par l'appuyer-pour-parler du
// moteur : celui-ci lit l'état de l'action « voice », qui restait enfoncée quand la
// touche était relâchée pendant qu'une zone de saisie avait le clavier (téléphone,
// menu). Le micro restait alors ouvert, notre anneau figé et le témoin de Steam
// allumé (Julien, 29/10/2026). On lit la vraie touche du clavier (celle choisie dans
// Keybinds), et jamais pendant une saisie de texte.
// Un appel décroché, n'importe lequel : CallOf rend le premier appel du joueur, et un
// ancien (sonnerie, refus) resté devant laissait le micro fermé — il fallait V pour être
// entendu en appel (Julien, 30/10/2026). Le même test que Talking, côté auditeur.
var inCall = phone.Calls.Any( x => x.Active && x.Involves( speaker.SteamId ) );
var key = Keybinds.KeyOf( "Voice" );
var pushing = !string.IsNullOrEmpty( key ) && Input.Keyboard.Down( key )
&& Sandbox.UI.InputFocus.Current is null;
if ( Mode != ActivateMode.Manual )
Mode = ActivateMode.Manual;
var listen = inCall || pushing;
if ( IsListening != listen )
IsListening = listen;
return;
}
var listener = RoleplayPlayer.Local;
var onThePhone = listener is not null && phone.Talking( speaker.SteamId, listener.SteamId );
WorldspacePlayback = !onThePhone;
Distance = ProximityRange;
}
/// <summary>
/// Hors appel, on n'envoie sa voix qu'aux joueurs à portée : inutile de
/// charger le réseau pour quelqu'un à l'autre bout de la ville.
/// </summary>
protected override IEnumerable<Connection> ExcludeFilter()
{
var speaker = GetComponent<RoleplayPlayer>();
var phone = PhoneSystem.Current;
if ( speaker is null || phone is null )
return Enumerable.Empty<Connection>();
// Dans la ruelle de la création, personne ne s'entend : on n'y est pas encore en jeu.
return RoleplayPlayer.All
.Where( x => x != speaker
&& ((x.WorldPosition.Distance( speaker.WorldPosition ) > ProximityRange * 1.2f && !phone.Talking( speaker.SteamId, x.SteamId ))
|| x.NeedsCharacter || speaker.NeedsCharacter) )
.Select( x => x.Network.Owner )
.Where( x => x is not null )
.ToList();
}
}