PartituraCaras.cs

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.

File Access
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;
	}
}