Component that controls a character face timetable (partitura). It stores four 48-slot tracks (LoopA/B, HorrorA/B) and on each frame computes which face index should be shown based on the global DirectorRitmo clock and phase, carrying the last marker forward until the next marker.
namespace SBH;
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// [SBHBase] Partitura de caras: la línea de tiempo de la cara de un
/// personaje, clavada al pulso del DirectorRitmo — como la pantalla de
/// Mr. Fun Computer en el original, que cambia de imagen con la letra.
/// Cada pista es el pulso de 4.8s dividido en 48 casilleros de 0.1s
/// (la resolución del original). Pintar una cara en un casillero es clavar
/// un MARCADOR: esa cara se sostiene hasta el próximo marcador; -1 = vacío.
/// Antes del primer marcador del pulso se sostiene la cola del anterior,
/// igual que los costumes del Scratch.
/// La cara NO se maneja con timers: cada frame se calcula del reloj global,
/// así TODAS las instancias del personaje muestran la misma cara en el mismo
/// instante, sin drift, incluso las spawneadas a mitad de pulso.
/// Se edita con el editor visual del menú C ("Partitura") — el componente lo
/// inyecta PartituraInyector a todo sprunki y carga lo guardado por personaje
/// (PartituraStore). Quien aplica la cara al bodygroup es ExpresionesNextbot
/// vía el puente del juego (las librerías no se conocen).
/// </summary>
public sealed class PartituraCaras : Component
{
/// <summary> Casilleros por pulso: 4.8s / 48 = 0.1s de resolución. </summary>
public const int Casilleros = 48;
[Property, Description( "Personaje al que pertenece la partitura (lo setea el inyector; clave del guardado)" )]
public string Personaje { get; set; } = "";
// Las pistas se editan con el editor visual del menú C, no acá
[Property, Hide] public List<int> LoopA { get; set; } = PistaVacia();
[Property, Hide] public List<int> LoopB { get; set; } = PistaVacia();
[Property, Hide] public List<int> HorrorA { get; set; } = PistaVacia();
[Property, Hide] public List<int> HorrorB { get; set; } = PistaVacia();
/// <summary>
/// La cara que dicta la partitura en este instante según el reloj global.
/// -1 = sin partitura ahora (reloj parado, silencio de cambio de fase,
/// cadáver o pistas sin pintar) — la vida facial normal sigue su curso.
/// </summary>
public int CaraActual { get; private set; } = -1;
DirectorRitmo _director;
public static List<int> PistaVacia()
{
var pista = new List<int>( Casilleros );
for ( var i = 0; i < Casilleros; i++ ) pista.Add( -1 );
return pista;
}
protected override void OnEnabled()
{
_director = Scene.GetSystem<DirectorRitmo>();
}
protected override void OnDisabled()
{
CaraActual = -1;
}
protected override void OnUpdate()
{
CaraActual = Calcular();
}
int Calcular()
{
// Reloj parado (ritmo apagado) o en el silencio del cambio de fase
if ( _director == null || _director.NumeroPulso < 0 ) return -1;
// El cadáver no sigue cantando (mismo tag que usa MuerteNextbot)
if ( GameObject.Tags.Has( "sbh_muerto" ) ) return -1;
var esA = _director.NumeroPulso % 2 == 0;
var pista = Elegir( esA );
if ( pista == null ) return -1;
var celda = ((int)(_director.FraccionPulso * Casilleros)).Clamp( 0, Casilleros - 1 );
// La cara vigente = el último marcador a la izquierda (incluido)
for ( var i = System.Math.Min( celda, pista.Count - 1 ); i >= 0; i-- )
{
if ( pista[i] >= 0 ) return pista[i];
}
// Antes del primer marcador: sostener la cola del pulso anterior
var anterior = Elegir( !esA );
if ( anterior != null )
{
for ( var i = anterior.Count - 1; i >= 0; i-- )
{
if ( anterior[i] >= 0 ) return anterior[i];
}
}
return -1;
}
List<int> Elegir( bool esA )
{
if ( _director.FaseHorror )
{
var horror = esA
? (TienePintado( HorrorA ) ? HorrorA : HorrorB)
: (TienePintado( HorrorB ) ? HorrorB : HorrorA);
if ( TienePintado( horror ) ) return horror;
// Sin partitura horror: cae a la normal (mismo criterio que RitmoPersonaje)
}
var normal = esA
? (TienePintado( LoopA ) ? LoopA : LoopB)
: (TienePintado( LoopB ) ? LoopB : LoopA);
return TienePintado( normal ) ? normal : null;
}
static bool TienePintado( List<int> pista )
{
if ( pista == null ) return false;
foreach ( var c in pista )
{
if ( c >= 0 ) return true;
}
return false;
}
}