Utility static class that transforms a Sprunki nextbot into another character by swapping its skinned model, cleaning up obsolete components, setting a display name, and spawning a colored fusion particle effect. It also exposes a console command to fuse the nextbot the camera is pointing at and an event fired after a successful fusion.
namespace SBH;
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// [SBHBase] Fusión de carteles estilo Incredibox: transformar un sprunki
/// en otro personaje del elenco es, en el fondo, cambiarle el modelo del
/// renderer. Todo lo demás se acomoda solo — el animgraph compartido viene
/// en el vmdl nuevo, y el RitmoInyector identifica el modelo nuevo y le
/// inyecta su loop (acá se destruye el RitmoPersonaje viejo para que
/// re-inyecte en el próximo escaneo, menos de un segundo).
/// La UI de carteles del menú C consume esto; mientras tanto se puede probar
/// desde consola: <c>sbh_fusion gray</c> transforma al sprunki que estés
/// mirando.
/// </summary>
public static class FusionNextbot
{
/// <summary> Disparado tras cada fusión exitosa (lo consumen los logros del juego). </summary>
public static event System.Action AlFusionar;
/// <summary>
/// Color característico de cada personaje (extraído del relleno de su
/// cartel en el juego original). Tiñe la nube del poof de fusión; también
/// puede servir para HUD, luces o lo que venga.
/// </summary>
public static readonly Dictionary<string, Color> Colores = new()
{
["sprunki_oren"] = Color.FromBytes( 255, 111, 0 ),
["sprunki_raddy"] = Color.FromBytes( 204, 0, 0 ),
["sprunki_clukr"] = Color.FromBytes( 186, 190, 191 ),
["sprunki_funbot"] = Color.FromBytes( 255, 198, 51 ),
["sprunki_vineria"] = Color.FromBytes( 0, 255, 21 ),
["sprunki_gray"] = Color.FromBytes( 128, 128, 128 ),
["sprunki_brud"] = Color.FromBytes( 128, 68, 0 ),
["sprunki_garnold"] = Color.FromBytes( 255, 198, 51 ),
["sprunki_owakcx"] = Color.FromBytes( 165, 255, 0 ),
["sprunki_sky"] = Color.FromBytes( 77, 246, 255 ),
["sprunki_mrsun"] = Color.FromBytes( 255, 233, 0 ),
["sprunki_durple"] = Color.FromBytes( 177, 0, 255 ),
["sprunki_mrtree"] = Color.FromBytes( 0, 204, 17 ),
["sprunki_simon"] = Color.FromBytes( 255, 233, 0 ),
["sprunki_tunner"] = Color.FromBytes( 230, 190, 138 ),
["sprunki_mrfuncomputer"] = Color.FromBytes( 186, 190, 191 ),
["sprunki_wenda"] = Color.FromBytes( 255, 255, 255 ),
["sprunki_pinki"] = Color.FromBytes( 255, 128, 238 ),
["sprunki_jevin"] = Color.FromBytes( 40, 30, 255 ),
["sprunki_black"] = Color.FromBytes( 45, 45, 55 ),
};
/// <summary>
/// Transforma el bot en el personaje pedido. Acepta nombre canónico
/// ("oren", "gray", "mrsun"...) o completo ("sprunki_oren"). Devuelve
/// false si el bot no vale o el modelo del personaje no existe todavía.
/// </summary>
public static bool Transformar( GameObject bot, string personaje, string nombreVisible = null )
{
if ( !bot.IsValid() || string.IsNullOrWhiteSpace( personaje ) ) return false;
var nombre = personaje.Trim().ToLowerInvariant();
if ( !nombre.StartsWith( "sprunki_" ) ) nombre = "sprunki_" + nombre;
var modelo = Model.Load( $"models/sprunkis/{nombre}.vmdl" );
if ( modelo == null || modelo.IsError )
{
Log.Warning( $"FusionNextbot: no existe models/sprunkis/{nombre}.vmdl (¿personaje sin modelar aún?)" );
return false;
}
var renderer = bot.Components.Get<SkinnedModelRenderer>();
if ( !renderer.IsValid() ) return false;
renderer.Model = modelo;
// El ritmo viejo ya no corresponde: fuera — el RitmoInyector detecta
// el vmdl nuevo y le inyecta el loop del personaje en el próximo escaneo
bot.Components.Get<RitmoPersonaje>( true )?.Destroy();
// La partitura de caras tampoco: era coreografía del personaje viejo
// (caras equivocadas en el cuerpo nuevo es peor que sin partitura)
bot.Components.Get<PartituraCaras>( true )?.Destroy();
// Y el bailecito del Clukr (si lo había): huesos de otro cuerpo
bot.Components.Get<BaileProcedural>( true )?.Destroy();
// Nombre visible (kill feed): el canónico si la UI lo pasa, o el
// nombre del modelo capitalizado como fallback (comando de consola)
if ( !string.IsNullOrWhiteSpace( nombreVisible ) )
{
bot.Name = nombreVisible;
}
else
{
var visible = nombre.Substring( "sprunki_".Length );
bot.Name = char.ToUpperInvariant( visible[0] ) + visible.Substring( 1 );
}
// El poof de la transformación: prefab one-shot en el torso (se
// limpia solo al terminar, DestroyOnEnd del emisor). La nube sale
// del color del personaje RESULTANTE — naranja para Oren, etc.
var color = Colores.GetValueOrDefault( nombre, Color.White );
EfectoFusion( bot.WorldPosition + Vector3.Up * 40f, color );
Log.Info( $"🃏 Fusión: el bot ahora es {bot.Name} ({nombre})" );
// Aviso para quien quiera festejar la fusión (logros del juego, 2026-08-10)
AlFusionar?.Invoke();
return true;
}
static void EfectoFusion( Vector3 posicion, Color color )
{
const string ruta = "prefabs/efectos/fx_fusion.prefab";
// OJO: StartEnabled explícito — el default de CloneConfig clona APAGADO
// (por eso la granada de sandbox enciende el clon a mano)
var config = new CloneConfig { Transform = new Transform( posicion ), StartEnabled = true };
var prefab = ResourceLibrary.Get<PrefabFile>( ruta );
GameObject go = null;
if ( prefab != null )
go = GameObject.Clone( prefab, config );
if ( !go.IsValid() )
go = GameObject.Clone( ruta, config );
if ( !go.IsValid() )
{
Log.Warning( $"FusionNextbot: no pude cargar {ruta} — el poof de fusión no va a verse" );
return;
}
// Cinturón por si algún camino de clonado ignora StartEnabled
go.Enabled = true;
// El DestroyOnEnd del emisor limpia las capas, pero el objeto RAÍZ
// del prefab quedaba vivo para siempre y la escena se llenaba de
// huérfanos — a los 3 segundos (las partículas viven ~1) se va entero
// (TemporaryEffect: el mismo componente del engine que usan los gibs)
go.Components.Create<TemporaryEffect>().DestroyAfterSeconds = 3f;
// Teñir la(s) capa(s) "Nube" con el color del personaje: el Tint
// multiplica el color base (casi blanco), así la nube sale del color
// pedido. Los destellos confeti quedan como están.
foreach ( var particulas in go.GetComponentsInChildren<ParticleEffect>() )
{
if ( particulas.GameObject.Name.StartsWith( "Nube" ) )
particulas.Tint = color;
}
}
/// <summary>
/// Prueba rápida sin UI: transforma el nextbot sprunki que la cámara está
/// mirando. Ej.: <c>sbh_fusion clukr</c>
/// </summary>
[ConCmd( "sbh_fusion" )]
public static void ComandoFusion( string personaje )
{
var scene = Game.ActiveScene;
var camara = scene?.Camera;
if ( camara == null ) return;
var trace = scene.Trace
.Ray( camara.WorldPosition, camara.WorldPosition + camara.WorldRotation.Forward * 4096f )
.IgnoreGameObjectHierarchy( camara.GameObject.Root )
.Run();
var root = trace.GameObject?.Root;
if ( !root.IsValid() || !root.Tags.Has( "sbh_nextbot" ) )
{
Log.Info( "sbh_fusion: apuntá a un nextbot sprunki" );
return;
}
Transformar( root, personaje );
}
}