Static helper for the downed-player heartbeat. Plays the heartbeat loop on a 5.28 s cycle, plays the flatline sound in the last 6 seconds, and exposes a 0-1 Pulse value the HUD reads to flash the screen on each beat.
namespace HorizonRP;
/// <summary>
/// Chez l'inconscient seulement : son cœur. Le fichier (sounds/mort/heartbeat.mp3, 4,9 s) porte
/// quatre battements, un toutes les 1,3 s, chacun fait d'un « boum » puis d'un « ba » 0,3 s plus
/// tard (mesurés sur l'enveloppe du son). Bouclé tel quel, le battement suivant revenait trop tôt :
/// on le relance donc soi-même toutes les 5,28 s, et le rythme reste régulier.
///
/// L'écran pulse exactement sur ces instants (<see cref="Pulse"/>, lu par le HUD) : un éclat sur le
/// « boum », un plus faible sur le « ba ». Dans les dernières secondes, le cœur s'arrête : le son
/// du tracé plat (heart_flatline.mp3, 5,9 s) et plus de pulsation.
/// </summary>
public static class Heartbeat
{
const string BeatFile = "sounds/mort/heartbeat.mp3";
const string FlatlineFile = "sounds/mort/heart_flatline.mp3";
const float Cycle = 5.28f;
const float FlatlineSeconds = 6f;
/// <summary>Les « boum » dans le fichier ; chaque « ba » suit de <see cref="Dub"/>.</summary>
static readonly float[] Lubs = { 0.02f, 1.38f, 2.68f, 3.98f };
const float Dub = 0.3f;
/// <summary>L'éclat de l'écran, de 0 à 1.</summary>
public static float Pulse { get; private set; }
/// <summary>Le tracé plat est en cours : l'écran s'éteint doucement au lieu de pulser.</summary>
public static bool Flatlining { get; private set; }
static SoundHandle beat, flat;
static RealTimeSince sinceCycle = 100;
static bool active;
/// <summary>Chaque image, chez le joueur local (RoleplayPlayer.UpdateDowned).</summary>
public static void Update( RoleplayPlayer me )
{
if ( me is null || !me.Downed )
{
Stop();
return;
}
if ( !active )
{
active = true;
Flatlining = false;
sinceCycle = 100;
}
// Les dernières secondes : le cœur lâche.
if ( me.DownedLeft <= FlatlineSeconds )
{
if ( !Flatlining )
{
Flatlining = true;
beat?.Stop( 0.05f );
beat = null;
flat = Sounds.PlayFile( FlatlineFile, 0.7f );
}
Pulse = 0;
return;
}
if ( sinceCycle >= Cycle )
{
sinceCycle = 0;
beat = Sounds.PlayFile( BeatFile, 0.8f );
}
// L'éclat : monte d'un coup sur chaque battement, retombe en 0,18 s.
var t = (float)sinceCycle;
var pulse = 0f;
foreach ( var lub in Lubs )
{
pulse = MathF.Max( pulse, Flash( t - lub, 1f ) );
pulse = MathF.Max( pulse, Flash( t - lub - Dub, 0.55f ) );
}
Pulse = pulse;
}
static float Flash( float since, float strength ) =>
since < 0 || since > 0.6f ? 0 : strength * MathF.Exp( -since / 0.18f );
static void Stop()
{
if ( !active )
return;
active = false;
Flatlining = false;
Pulse = 0;
beat?.Stop( 0.2f );
flat?.Stop( 0.3f );
beat = flat = null;
}
}