A static manager for fusion recipes. It enumerates pack-provided .fusion assets and user-created recipes persisted as JSON in FileSystem.Data, provides CRUD methods (Guardar, Borrar, BorrarTodas), and exposes Todas() and a Version counter for UI rebuilds.
namespace SBH;
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// [SBHBase] El libro de recetas de fusión. Junta DOS fuentes:
/// - Las recetas de PACK: assets .fusion (RecetaFusion), solo lectura.
/// - Las recetas PROPIAS: creadas en el juego con la tool Merge en modo
/// crear; persisten como JSON en FileSystem.Data y sobreviven sesiones.
/// La tool y el recetario consumen Todas() sin distinguir origen (salvo
/// para permitir borrar: solo las propias).
/// </summary>
public static class LibroFusiones
{
const string Archivo = "sbh_fusiones.json";
/// <summary> Una receta lista para usar, venga del pack o del juego. </summary>
public class Receta
{
public List<IngredienteFusion> Ingredientes { get; set; } = new();
/// <summary> Ruta del prefab que aparece al fusionar. </summary>
public string RutaResultado { get; set; } = "";
/// <summary> Ruta de un SoundEvent opcional. </summary>
public string RutaSonido { get; set; }
/// <summary> Vino de un asset .fusion: no se puede borrar desde el juego. </summary>
public bool DePack { get; set; }
}
static List<Receta> _propias;
/// <summary> Sube con cada alta/baja de receta propia — para BuildHash de la UI. </summary>
public static int Version { get; private set; }
/// <summary> Todas las recetas válidas: primero las del pack, después las propias. </summary>
public static IEnumerable<Receta> Todas()
{
foreach ( var r in ResourceLibrary.GetAll<RecetaFusion>() )
{
if ( r.Resultado == null ) continue;
if ( r.Ingredientes == null || r.Ingredientes.Count == 0 ) continue;
yield return new Receta
{
Ingredientes = r.Ingredientes,
RutaResultado = r.Resultado.ResourcePath,
RutaSonido = r.Sonido?.ResourcePath,
DePack = true,
};
}
foreach ( var r in Propias() )
yield return r;
}
static List<Receta> Propias()
{
if ( _propias != null ) return _propias;
_propias = new List<Receta>();
try
{
if ( FileSystem.Data.FileExists( Archivo ) )
_propias = Json.Deserialize<List<Receta>>( FileSystem.Data.ReadAllText( Archivo ) ) ?? new();
}
catch ( Exception e )
{
Log.Warning( $"LibroFusiones: no pude leer {Archivo} — {e.Message}" );
}
foreach ( var r in _propias )
r.DePack = false;
return _propias;
}
/// <summary>
/// Guarda una receta creada en el juego. Si ya había una propia con los
/// MISMOS ingredientes, se reemplaza — re-grabar una receta es la forma
/// de cambiarle el resultado.
/// </summary>
public static void Guardar( Receta receta )
{
var propias = Propias();
propias.RemoveAll( r => MismosIngredientes( r, receta ) );
propias.Add( receta );
Persistir();
}
/// <summary> Borra una receta propia (las de pack no se tocan). </summary>
public static void Borrar( Receta receta )
{
if ( receta.DePack ) return;
Propias().RemoveAll( r => MismosIngredientes( r, receta )
&& r.RutaResultado == receta.RutaResultado );
Persistir();
}
/// <summary> El botón nuclear: borra TODAS las recetas propias de un saque. </summary>
public static void BorrarTodas()
{
Propias().Clear();
Persistir();
}
static void Persistir()
{
try
{
FileSystem.Data.WriteAllText( Archivo, Json.Serialize( _propias ) );
}
catch ( Exception e )
{
Log.Warning( $"LibroFusiones: no pude guardar {Archivo} — {e.Message}" );
}
Version++;
}
// Mismo multiset de ingredientes (nombres sin mayúsculas, cantidades sumadas)
static bool MismosIngredientes( Receta a, Receta b )
{
static Dictionary<string, int> Mapa( Receta r ) => r.Ingredientes
.GroupBy( i => i.Nombre?.Trim().ToLowerInvariant() ?? "" )
.ToDictionary( g => g.Key, g => g.Sum( i => Math.Max( 1, i.Cantidad ) ) );
var ma = Mapa( a );
var mb = Mapa( b );
return ma.Count == mb.Count
&& ma.All( kv => mb.TryGetValue( kv.Key, out var c ) && c == kv.Value );
}
}