A game-wide rhythm director GameObjectSystem. It tracks a repeating pulse every 4.8 seconds, handles a 2s silence when a "horror phase" flag changes, exposes current pulse number and fraction through properties, and raises an event AlPulso<int> on each pulse.
namespace SBH;
using Sandbox;
using System;
/// <summary>
/// [SBHBase] El reloj del juego original, calcado del "loop controller"
/// del Sprunki de Scratch: un pulso cada 4.8 segundos (100 BPM, 2 compases
/// de 4/4) alternando "Loop 1" y "Loop 2". Los personajes RE-DISPARAN su
/// sonido en cada pulso — la sincronía es por retrigger, no hay loops
/// continuos que mantener en fase.
/// Cambio de fase (horror): corta todo, espera 2s y rearranca el reloj en
/// el pulso 0, igual que el broadcast "Phase 2" del original.
/// Es un GameObjectSystem: existe solo en toda escena, sin setup.
/// </summary>
public sealed class DirectorRitmo : GameObjectSystem
{
/// <summary> Segundos entre pulsos: la variable Delay = 4.8 del original. </summary>
public const float DuracionPulso = 4.8f;
/// <summary> Silencio al cambiar de fase, como el wait 2 del original. </summary>
public const float SilencioCambioFase = 2f;
/// <summary> Número de pulso actual (0, 1, 2...); par = "Loop 1", impar = "Loop 2". -1 = todavía no arrancó. </summary>
public int NumeroPulso { get; private set; } = -1;
/// <summary> Fase horror activa (espejo estable de RitmoConVars.FaseHorror). </summary>
public bool FaseHorror { get; private set; }
/// <summary> Se dispara en cada pulso con su número. Los RitmoPersonaje se suscriben acá. </summary>
public event Action<int> AlPulso;
float _proximoPulso;
float _silencioHasta = -1f;
public DirectorRitmo( Scene scene ) : base( scene )
{
Listen( Stage.StartUpdate, 10, Latir, "DirectorRitmoPersonaje" );
_proximoPulso = Time.Now;
}
/// <summary> Avance 0..1 dentro del pulso actual (para sincronizar bailes). </summary>
public float FraccionPulso => 1f - ((_proximoPulso - Time.Now) / DuracionPulso).Clamp( 0f, 1f );
void Latir()
{
// Apagado: reloj detenido; al reactivar arranca limpio desde el pulso 0
if ( !RitmoConVars.RitmoActivado )
{
NumeroPulso = -1;
_proximoPulso = Time.Now;
return;
}
// Cambio de fase estilo "Phase 2": corte, 2s de silencio y reloj a cero
if ( RitmoConVars.FaseHorror != FaseHorror )
{
FaseHorror = RitmoConVars.FaseHorror;
NumeroPulso = -1;
_silencioHasta = Time.Now + SilencioCambioFase;
_proximoPulso = Time.Now + SilencioCambioFase;
}
if ( Time.Now < _silencioHasta ) return;
if ( Time.Now >= _proximoPulso )
{
NumeroPulso++;
_proximoPulso += DuracionPulso;
// Tras una pausa larga (editor, hitch) no disparar ráfagas de pulsos
if ( Time.Now >= _proximoPulso )
_proximoPulso = Time.Now + DuracionPulso;
AlPulso?.Invoke( NumeroPulso );
}
}
}