{"TotalCount":73,"Files":[{"Ident":"ksbravo.sbhbase","Path":"Code/HuesosManipulados.cs","FileName":"HuesosManipulados.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Huesos manipulados a mano (los ManipulateBone* de GMod, para el\n/// Advanced Bone Tool): guarda un ajuste por hueso (\u00E1ngulos/posici\u00F3n/escala,\n/// offsets en el MARCO LOCAL del hueso) y lo aplica cada frame POR ENCIMA de\n/// la animaci\u00F3n ya evaluada \u2014 patr\u00F3n BaileProcedural: TryGetBoneTransformAnimation\n/// lee la pose SIN overrides (sin retroalimentaci\u00F3n) y SetBoneOverride pisa.\n/// Con la lista vac\u00EDa no toca nada (la animaci\u00F3n queda pura). En un bot\n/// muerto (tag sbh_muerto) se apaga: el ragdoll manda.\n/// OJO: comparte SceneModel.ClearBoneOverrides con otros drivers de huesos\n/// (ej. BaileProcedural) \u2014 dos drivers vivos en el mismo bot se pisan entre s\u00ED.\n/// \u003C/summary\u003E\npublic sealed class HuesosManipulados : Component\n{\n\tpublic struct Ajuste\n\t{\n\t\tpublic Angles Angulos;\n\t\tpublic Vector3 Posicion;\n\t\tpublic float Escala;\n\n\t\tpublic static readonly Ajuste Identidad = new() { Escala = 1f };\n\n\t\tpublic bool EsIdentidad =\u003E\n\t\t\tAngulos == Angles.Zero \u0026\u0026 Posicion == Vector3.Zero \u0026\u0026 Escala == 1f;\n\t}\n\n\treadonly Dictionary\u003Cstring, Ajuste\u003E _ajustes = new();\n\tSkinnedModelRenderer _renderer;\n\tbool _aplicado;\n\n\tpublic IReadOnlyDictionary\u003Cstring, Ajuste\u003E Ajustes =\u003E _ajustes;\n\n\tpublic Ajuste De( string hueso )\n\t\t=\u003E hueso != null \u0026\u0026 _ajustes.TryGetValue( hueso, out var a ) ? a : Ajuste.Identidad;\n\n\tpublic void Fijar( string hueso, Ajuste ajuste )\n\t{\n\t\tif ( string.IsNullOrEmpty( hueso ) ) return;\n\n\t\tif ( ajuste.EsIdentidad )\n\t\t\t_ajustes.Remove( hueso );\n\t\telse\n\t\t\t_ajustes[hueso] = ajuste;\n\t}\n\n\tpublic void Reiniciar() =\u003E _ajustes.Clear();\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_renderer = Components.Get\u003CSkinnedModelRenderer\u003E();\n\t}\n\n\tprotected override void OnDisabled() =\u003E Limpiar();\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !_renderer.IsValid() || _renderer.SceneModel == null ) return;\n\n\t\tif ( _ajustes.Count == 0 || GameObject.Tags.Has( \u0022sbh_muerto\u0022 ) )\n\t\t{\n\t\t\tLimpiar();\n\t\t\treturn;\n\t\t}\n\n\t\tvar sm = _renderer.SceneModel;\n\t\tsm.ClearBoneOverrides();\n\n\t\tforeach ( var (nombre, ajuste) in _ajustes )\n\t\t{\n\t\t\tvar hueso = _renderer.Model?.Bones?.GetBone( nombre );\n\t\t\tif ( hueso == null ) continue;\n\n\t\t\t// Pose de la ANIMACI\u00D3N (mundo), sin nuestros overrides\n\t\t\tif ( !_renderer.TryGetBoneTransformAnimation( hueso, out var mundo ) ) continue;\n\n\t\t\tvar local = sm.Transform.ToLocal( mundo );\n\n\t\t\t// Offsets en el marco local del hueso (como ManipulateBone* de GMod)\n\t\t\tlocal.Position \u002B= local.Rotation * ajuste.Posicion;\n\t\t\tlocal.Rotation *= Rotation.From( ajuste.Angulos );\n\t\t\tlocal.Scale *= ajuste.Escala;\n\n\t\t\tsm.SetBoneOverride( hueso.Index, local );\n\t\t}\n\n\t\t_aplicado = true;\n\t}\n\n\tvoid Limpiar()\n\t{\n\t\tif ( !_aplicado ) return;\n\t\t_aplicado = false;\n\n\t\tif ( _renderer.IsValid() \u0026\u0026 _renderer.SceneModel != null )\n\t\t\t_renderer.SceneModel.ClearBoneOverrides();\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"Code/LibroFusiones.cs","FileName":"LibroFusiones.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// [SBHBase] El libro de recetas de fusi\u00F3n. Junta DOS fuentes:\n///  - Las recetas de PACK: assets .fusion (RecetaFusion), solo lectura.\n///  - Las recetas PROPIAS: creadas en el juego con la tool Merge en modo\n///    crear; persisten como JSON en FileSystem.Data y sobreviven sesiones.\n/// La tool y el recetario consumen Todas() sin distinguir origen (salvo\n/// para permitir borrar: solo las propias).\n/// \u003C/summary\u003E\npublic static class LibroFusiones\n{\n\tconst string Archivo = \u0022sbh_fusiones.json\u0022;\n\n\t/// \u003Csummary\u003E Una receta lista para usar, venga del pack o del juego. \u003C/summary\u003E\n\tpublic class Receta\n\t{\n\t\tpublic List\u003CIngredienteFusion\u003E Ingredientes { get; set; } = new();\n\n\t\t/// \u003Csummary\u003E Ruta del prefab que aparece al fusionar. \u003C/summary\u003E\n\t\tpublic string RutaResultado { get; set; } = \u0022\u0022;\n\n\t\t/// \u003Csummary\u003E Ruta de un SoundEvent opcional. \u003C/summary\u003E\n\t\tpublic string RutaSonido { get; set; }\n\n\t\t/// \u003Csummary\u003E Vino de un asset .fusion: no se puede borrar desde el juego. \u003C/summary\u003E\n\t\tpublic bool DePack { get; set; }\n\t}\n\n\tstatic List\u003CReceta\u003E _propias;\n\n\t/// \u003Csummary\u003E Sube con cada alta/baja de receta propia \u2014 para BuildHash de la UI. \u003C/summary\u003E\n\tpublic static int Version { get; private set; }\n\n\t/// \u003Csummary\u003E Todas las recetas v\u00E1lidas: primero las del pack, despu\u00E9s las propias. \u003C/summary\u003E\n\tpublic static IEnumerable\u003CReceta\u003E Todas()\n\t{\n\t\tforeach ( var r in ResourceLibrary.GetAll\u003CRecetaFusion\u003E() )\n\t\t{\n\t\t\tif ( r.Resultado == null ) continue;\n\t\t\tif ( r.Ingredientes == null || r.Ingredientes.Count == 0 ) continue;\n\n\t\t\tyield return new Receta\n\t\t\t{\n\t\t\t\tIngredientes = r.Ingredientes,\n\t\t\t\tRutaResultado = r.Resultado.ResourcePath,\n\t\t\t\tRutaSonido = r.Sonido?.ResourcePath,\n\t\t\t\tDePack = true,\n\t\t\t};\n\t\t}\n\n\t\tforeach ( var r in Propias() )\n\t\t\tyield return r;\n\t}\n\n\tstatic List\u003CReceta\u003E Propias()\n\t{\n\t\tif ( _propias != null ) return _propias;\n\n\t\t_propias = new List\u003CReceta\u003E();\n\n\t\ttry\n\t\t{\n\t\t\tif ( FileSystem.Data.FileExists( Archivo ) )\n\t\t\t\t_propias = Json.Deserialize\u003CList\u003CReceta\u003E\u003E( FileSystem.Data.ReadAllText( Archivo ) ) ?? new();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022LibroFusiones: no pude leer {Archivo} \u2014 {e.Message}\u0022 );\n\t\t}\n\n\t\tforeach ( var r in _propias )\n\t\t\tr.DePack = false;\n\n\t\treturn _propias;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Guarda una receta creada en el juego. Si ya hab\u00EDa una propia con los\n\t/// MISMOS ingredientes, se reemplaza \u2014 re-grabar una receta es la forma\n\t/// de cambiarle el resultado.\n\t/// \u003C/summary\u003E\n\tpublic static void Guardar( Receta receta )\n\t{\n\t\tvar propias = Propias();\n\t\tpropias.RemoveAll( r =\u003E MismosIngredientes( r, receta ) );\n\t\tpropias.Add( receta );\n\t\tPersistir();\n\t}\n\n\t/// \u003Csummary\u003E Borra una receta propia (las de pack no se tocan). \u003C/summary\u003E\n\tpublic static void Borrar( Receta receta )\n\t{\n\t\tif ( receta.DePack ) return;\n\n\t\tPropias().RemoveAll( r =\u003E MismosIngredientes( r, receta )\n\t\t\t\u0026\u0026 r.RutaResultado == receta.RutaResultado );\n\t\tPersistir();\n\t}\n\n\t/// \u003Csummary\u003E El bot\u00F3n nuclear: borra TODAS las recetas propias de un saque. \u003C/summary\u003E\n\tpublic static void BorrarTodas()\n\t{\n\t\tPropias().Clear();\n\t\tPersistir();\n\t}\n\n\tstatic void Persistir()\n\t{\n\t\ttry\n\t\t{\n\t\t\tFileSystem.Data.WriteAllText( Archivo, Json.Serialize( _propias ) );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022LibroFusiones: no pude guardar {Archivo} \u2014 {e.Message}\u0022 );\n\t\t}\n\n\t\tVersion\u002B\u002B;\n\t}\n\n\t// Mismo multiset de ingredientes (nombres sin may\u00FAsculas, cantidades sumadas)\n\tstatic bool MismosIngredientes( Receta a, Receta b )\n\t{\n\t\tstatic Dictionary\u003Cstring, int\u003E Mapa( Receta r ) =\u003E r.Ingredientes\n\t\t\t.GroupBy( i =\u003E i.Nombre?.Trim().ToLowerInvariant() ?? \u0022\u0022 )\n\t\t\t.ToDictionary( g =\u003E g.Key, g =\u003E g.Sum( i =\u003E Math.Max( 1, i.Cantidad ) ) );\n\n\t\tvar ma = Mapa( a );\n\t\tvar mb = Mapa( b );\n\n\t\treturn ma.Count == mb.Count\n\t\t\t\u0026\u0026 ma.All( kv =\u003E mb.TryGetValue( kv.Key, out var c ) \u0026\u0026 c == kv.Value );\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"Code/RitmoInyector.cs","FileName":"RitmoInyector.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Inyecta el RitmoPersonaje configurado a todo nextbot sprunki\n/// que aparezca en escena (tag \u0022sbh_nextbot\u0022), identificando al personaje\n/// por el nombre de su vmdl. Mismo patr\u00F3n que SbhPuente: las integraciones\n/// se inyectan en runtime y los prefabs de la librer\u00EDa base quedan limpios\n/// \u2014 SBHBase ni se entera de que existe el sistema de ritmos.\n/// El cat\u00E1logo cubre los 20 personajes can\u00F3nicos del juego original (ver\n/// MAPA_PERSONAJES.md en Work S\u0026BOX\\Pack-01-Sprunkis\\00-audios-ritmos).\n/// \u003C/summary\u003E\npublic sealed class RitmoInyector : GameObjectSystem\n{\n\tsealed record Loops( string NormalA, string NormalB, string HorrorA, string HorrorB );\n\n\t// clave = fragmento del nombre del vmdl \u00B7 rutas relativas a sounds/ritmos/\n\tstatic readonly Dictionary\u003Cstring, Loops\u003E Catalogo = new()\n\t{\n\t\t// Los tres ya modelados (con sus alias actuales orng/grey/red)\n\t\t[\u0022sprunki_oren\u0022] = new( \u0022orange_oren/kick\u0022, null, \u0022orange_oren/kick3\u0022, null ),\n\t\t[\u0022sprunki_orng\u0022] = new( \u0022orange_oren/kick\u0022, null, \u0022orange_oren/kick3\u0022, null ),\n\t\t[\u0022sprunki_raddy\u0022] = new( \u0022red_raddy/snare\u0022, null, \u0022red_raddy/snare3\u0022, null ),\n\t\t[\u0022sprunki_red\u0022] = new( \u0022red_raddy/snare\u0022, null, \u0022red_raddy/snare3\u0022, null ),\n\t\t[\u0022sprunki_gray\u0022] = new( \u0022gray_gray/bass1\u0022, \u0022gray_gray/bass2\u0022, \u0022gray_gray/bass3\u0022, \u0022gray_gray/bass4\u0022 ),\n\t\t[\u0022sprunki_grey\u0022] = new( \u0022gray_gray/bass1\u0022, \u0022gray_gray/bass2\u0022, \u0022gray_gray/bass3\u0022, \u0022gray_gray/bass4\u0022 ),\n\n\t\t// El resto del elenco can\u00F3nico, listos para cuando existan los modelos\n\t\t[\u0022sprunki_clukr\u0022] = new( \u0022silver_clukr/ride_cymbal\u0022, \u0022silver_clukr/ride_cymbal2\u0022, \u0022silver_clukr/knock_knocn\u0022, \u0022silver_clukr/knock_knocn2\u0022 ),\n\t\t[\u0022sprunki_funbot\u0022] = new( \u0022fun_bot/amen\u0022, null, \u0022fun_bot/amen3\u0022, null ),\n\t\t[\u0022sprunki_vineria\u0022] = new( \u0022green_vineria/shaky_shaky\u0022, null, \u0022green_vineria/shakey\u0022, null ),\n\t\t[\u0022sprunki_brud\u0022] = new( \u0022brown_brud/goofy_ahh_thing\u0022, null, \u0022brown_brud/weird_sound\u0022, \u0022brown_brud/weird_sound2\u0022 ),\n\t\t[\u0022sprunki_garnold\u0022] = new( \u0022gold_garnold/arpeg\u0022, null, \u0022gold_garnold/slow_arpeg\u0022, null ),\n\t\t[\u0022sprunki_owakcx\u0022] = new( \u0022lime_owakcx/wind_up1\u0022, \u0022lime_owakcx/wind_up2\u0022, \u0022lime_owakcx/better_windup\u0022, null ),\n\t\t[\u0022sprunki_sky\u0022] = new( \u0022sky_blue_sky/music_box1\u0022, \u0022sky_blue_sky/music_box2\u0022, \u0022sky_blue_sky/music1\u0022, \u0022sky_blue_sky/music2\u0022 ),\n\t\t[\u0022sprunki_mrsun\u0022] = new( \u0022mr_sun/piano1\u0022, \u0022mr_sun/piano2\u0022, \u0022mr_sun/piano_new1\u0022, \u0022mr_sun/piano_new2\u0022 ),\n\t\t[\u0022sprunki_durple\u0022] = new( \u0022purple_durple/tromp_new1\u0022, \u0022purple_durple/tromp_new2\u0022, \u0022purple_durple/horns1\u0022, \u0022purple_durple/horns2\u0022 ),\n\t\t[\u0022sprunki_mrtree\u0022] = new( \u0022mr_tree/organ1\u0022, \u0022mr_tree/organ2\u0022, \u0022mr_tree/slow_organ1\u0022, \u0022mr_tree/slow_organ2\u0022 ),\n\t\t[\u0022sprunki_simon\u0022] = new( \u0022yellow_simon/square1\u0022, \u0022yellow_simon/square2\u0022, \u0022yellow_simon/square_new1\u0022, \u0022yellow_simon/square_new2\u0022 ),\n\t\t[\u0022sprunki_tunner\u0022] = new( \u0022tan_tunner/whistle1\u0022, \u0022tan_tunner/whistle2\u0022, \u0022tan_tunner/whistle3\u0022, \u0022tan_tunner/whistle4\u0022 ),\n\t\t[\u0022sprunki_mrfuncomputer\u0022] = new( \u0022mr_fun_computer/aising1\u0022, \u0022mr_fun_computer/aising2\u0022, \u0022mr_fun_computer/aispeak1\u0022, \u0022mr_fun_computer/aispeak2\u0022 ),\n\t\t[\u0022sprunki_wenda\u0022] = new( \u0022white_wenda/boring_hey_sound\u0022, null, \u0022white_wenda/crazy\u0022, null ),\n\t\t[\u0022sprunki_pinki\u0022] = new( \u0022pink_pinki/femchoir1\u0022, \u0022pink_pinki/femchoir2\u0022, \u0022pink_pinki/choir1\u0022, \u0022pink_pinki/choir2\u0022 ),\n\t\t[\u0022sprunki_jevin\u0022] = new( \u0022blue_jevin/new_choir1\u0022, \u0022blue_jevin/new_choir2\u0022, \u0022blue_jevin/male_choir\u0022, \u0022blue_jevin/male_choir2\u0022 ),\n\n\t\t// El secreto: mudo en fase normal, solo existe en horror\n\t\t[\u0022sprunki_black\u0022] = new( null, null, \u0022blackx/creepy1\u0022, \u0022blackx/creepy2\u0022 ),\n\n\t\t// El Basic (polo en blanco, sin personaje): MUDO por dise\u00F1o en ambas\n\t\t// fases \u2014 la entrada existe para declararlo y que el inyector no avise\n\t\t[\u0022sprunki_base\u0022] = new( null, null, null, null ),\n\t\t[\u0022sprunki_hrr_base\u0022] = new( null, null, null, null ),\n\n\t\t// ---- Personajes HORROR (sprunki_hrr_*): personajes COMPLETOS aparte,\n\t\t// su loop \u0022normal\u0022 ES el sonido horror \u2014 cantan horror en toda fase\n\t\t// (como Black en el original). El prefijo hrr evita colisiones con el\n\t\t// Contains de los nombres base.\n\t\t[\u0022sprunki_hrr_oren\u0022] = new( \u0022orange_oren/kick3\u0022, null, \u0022orange_oren/kick3\u0022, null ),\n\t\t[\u0022sprunki_hrr_raddy\u0022] = new( \u0022red_raddy/snare3\u0022, null, \u0022red_raddy/snare3\u0022, null ),\n\t\t[\u0022sprunki_hrr_gray\u0022] = new( \u0022gray_gray/bass3\u0022, \u0022gray_gray/bass4\u0022, \u0022gray_gray/bass3\u0022, \u0022gray_gray/bass4\u0022 ),\n\t\t[\u0022sprunki_hrr_clukr\u0022] = new( \u0022silver_clukr/knock_knocn\u0022, \u0022silver_clukr/knock_knocn2\u0022, \u0022silver_clukr/knock_knocn\u0022, \u0022silver_clukr/knock_knocn2\u0022 ),\n\t\t[\u0022sprunki_hrr_funbot\u0022] = new( \u0022fun_bot/amen3\u0022, null, \u0022fun_bot/amen3\u0022, null ),\n\t\t[\u0022sprunki_hrr_vineria\u0022] = new( \u0022green_vineria/shakey\u0022, null, \u0022green_vineria/shakey\u0022, null ),\n\t\t[\u0022sprunki_hrr_brud\u0022] = new( \u0022brown_brud/weird_sound\u0022, \u0022brown_brud/weird_sound2\u0022, \u0022brown_brud/weird_sound\u0022, \u0022brown_brud/weird_sound2\u0022 ),\n\t\t[\u0022sprunki_hrr_garnold\u0022] = new( \u0022gold_garnold/slow_arpeg\u0022, null, \u0022gold_garnold/slow_arpeg\u0022, null ),\n\t\t[\u0022sprunki_hrr_owakcx\u0022] = new( \u0022lime_owakcx/better_windup\u0022, null, \u0022lime_owakcx/better_windup\u0022, null ),\n\t\t[\u0022sprunki_hrr_sky\u0022] = new( \u0022sky_blue_sky/music1\u0022, \u0022sky_blue_sky/music2\u0022, \u0022sky_blue_sky/music1\u0022, \u0022sky_blue_sky/music2\u0022 ),\n\t\t[\u0022sprunki_hrr_mrsun\u0022] = new( \u0022mr_sun/piano_new1\u0022, \u0022mr_sun/piano_new2\u0022, \u0022mr_sun/piano_new1\u0022, \u0022mr_sun/piano_new2\u0022 ),\n\t\t[\u0022sprunki_hrr_durple\u0022] = new( \u0022purple_durple/horns1\u0022, \u0022purple_durple/horns2\u0022, \u0022purple_durple/horns1\u0022, \u0022purple_durple/horns2\u0022 ),\n\t\t[\u0022sprunki_hrr_mrtree\u0022] = new( \u0022mr_tree/slow_organ1\u0022, \u0022mr_tree/slow_organ2\u0022, \u0022mr_tree/slow_organ1\u0022, \u0022mr_tree/slow_organ2\u0022 ),\n\t\t[\u0022sprunki_hrr_simon\u0022] = new( \u0022yellow_simon/square_new1\u0022, \u0022yellow_simon/square_new2\u0022, \u0022yellow_simon/square_new1\u0022, \u0022yellow_simon/square_new2\u0022 ),\n\t\t[\u0022sprunki_hrr_tunner\u0022] = new( \u0022tan_tunner/whistle3\u0022, \u0022tan_tunner/whistle4\u0022, \u0022tan_tunner/whistle3\u0022, \u0022tan_tunner/whistle4\u0022 ),\n\t\t[\u0022sprunki_hrr_mrfuncomputer\u0022] = new( \u0022mr_fun_computer/aispeak1\u0022, \u0022mr_fun_computer/aispeak2\u0022, \u0022mr_fun_computer/aispeak1\u0022, \u0022mr_fun_computer/aispeak2\u0022 ),\n\t\t[\u0022sprunki_hrr_wenda\u0022] = new( \u0022white_wenda/crazy\u0022, null, \u0022white_wenda/crazy\u0022, null ),\n\t\t[\u0022sprunki_hrr_pinki\u0022] = new( \u0022pink_pinki/choir1\u0022, \u0022pink_pinki/choir2\u0022, \u0022pink_pinki/choir1\u0022, \u0022pink_pinki/choir2\u0022 ),\n\t\t[\u0022sprunki_hrr_jevin\u0022] = new( \u0022blue_jevin/male_choir\u0022, \u0022blue_jevin/male_choir2\u0022, \u0022blue_jevin/male_choir\u0022, \u0022blue_jevin/male_choir2\u0022 ),\n\t\t[\u0022sprunki_hrr_black\u0022] = new( \u0022blackx/creepy1\u0022, \u0022blackx/creepy2\u0022, \u0022blackx/creepy1\u0022, \u0022blackx/creepy2\u0022 ),\n\t};\n\n\tTimeSince _desdeEscaneo;\n\treadonly HashSet\u003Cstring\u003E _sinCatalogo = new(); // para avisar una sola vez por modelo\n\n\tpublic RitmoInyector( Scene scene ) : base( scene )\n\t{\n\t\tListen( Stage.StartUpdate, 20, Escanear, \u0022RitmoInyectorSprunki\u0022 );\n\t}\n\n\tvoid Escanear()\n\t{\n\t\tif ( _desdeEscaneo \u003C 1f ) return;\n\t\t_desdeEscaneo = 0f;\n\n\t\tforeach ( var renderer in Scene.GetAllComponents\u003CSkinnedModelRenderer\u003E() )\n\t\t{\n\t\t\tvar go = renderer.GameObject;\n\t\t\tif ( !go.IsValid() || !go.Tags.Has( \u0022sbh_nextbot\u0022 ) ) continue;\n\t\t\t// Accesorios bonemerge (Easy Bonemerge de SBHBase): heredan el tag\n\t\t\t// del bot que visten, pero son decoraci\u00F3n \u2014 no cantan\n\t\t\tif ( go.Tags.Has( \u0022sbh_accesorio\u0022 ) ) continue;\n\t\t\tif ( go.Components.Get\u003CRitmoPersonaje\u003E( true ) != null ) continue;\n\n\t\t\tvar modelo = renderer.Model?.ResourcePath ?? \u0022\u0022;\n\t\t\tif ( modelo == \u0022\u0022 ) continue;\n\n\t\t\tvar enganchado = false;\n\n\t\t\tforeach ( var (clave, loops) in Catalogo )\n\t\t\t{\n\t\t\t\tif ( !modelo.Contains( clave ) ) continue;\n\t\t\t\tenganchado = true;\n\n\t\t\t\tvar ritmo = go.Components.Create\u003CRitmoPersonaje\u003E();\n\t\t\t\tritmo.LoopNormalA = Cargar( loops.NormalA );\n\t\t\t\tritmo.LoopNormalB = Cargar( loops.NormalB );\n\t\t\t\tritmo.LoopHorrorA = Cargar( loops.HorrorA );\n\t\t\t\tritmo.LoopHorrorB = Cargar( loops.HorrorB );\n\n\t\t\t\t// El Clukr (base y horror) trae su bailecito procedural: la tapa\n\t\t\t\t// rebota y los ojos giran mientras suena su loop\n\t\t\t\tif ( clave.Contains( \u0022clukr\u0022 ) \u0026\u0026 go.Components.Get\u003CBaileProcedural\u003E( true ) == null )\n\t\t\t\t\tgo.Components.Create\u003CBaileProcedural\u003E();\n\n\t\t\t\tLog.Info( $\u0022\uD83C\uDFB5 RitmoPersonaje inyectado a {go.Name} ({clave})\u0022 );\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Nextbot sprunki sin ritmo en el cat\u00E1logo: avisar una sola vez \u2014\n\t\t\t// casi siempre es un nombre de vmdl que no respeta el sprunki_\u003Cpersonaje\u003E.\n\t\t\t// Bots de OTROS packs (trevorhenderson, bfdi...) no son asunto de\n\t\t\t// este inyector: silencio total (multi-pack desde 2026-07-26)\n\t\t\tif ( !enganchado \u0026\u0026 modelo.Contains( \u0022sprunki\u0022 ) \u0026\u0026 _sinCatalogo.Add( modelo ) )\n\t\t\t\tLog.Warning( $\u0022RitmoInyector: \u0027{modelo}\u0027 no matchea ning\u00FAn personaje del cat\u00E1logo (el vmdl debe llamarse sprunki_\u003Cnombre can\u00F3nico\u003E, ver MAPA_PERSONAJES.md)\u0022 );\n\t\t}\n\t}\n\n\tstatic SoundEvent Cargar( string ruta )\n\t{\n\t\tif ( string.IsNullOrEmpty( ruta ) ) return null;\n\n\t\tvar sonido = ResourceLibrary.Get\u003CSoundEvent\u003E( $\u0022sounds/ritmos/{ruta}.sound\u0022 );\n\t\tif ( sonido == null )\n\t\t\tLog.Warning( $\u0022RitmoInyector: no existe sounds/ritmos/{ruta}.sound\u0022 );\n\n\t\treturn sonido;\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"DirectorRitmo.cs","FileName":"DirectorRitmo.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// [SBHBase] El reloj del juego original, calcado del \u0022loop controller\u0022\n/// del Sprunki de Scratch: un pulso cada 4.8 segundos (100 BPM, 2 compases\n/// de 4/4) alternando \u0022Loop 1\u0022 y \u0022Loop 2\u0022. Los personajes RE-DISPARAN su\n/// sonido en cada pulso \u2014 la sincron\u00EDa es por retrigger, no hay loops\n/// continuos que mantener en fase.\n/// Cambio de fase (horror): corta todo, espera 2s y rearranca el reloj en\n/// el pulso 0, igual que el broadcast \u0022Phase 2\u0022 del original.\n/// Es un GameObjectSystem: existe solo en toda escena, sin setup.\n/// \u003C/summary\u003E\npublic sealed class DirectorRitmo : GameObjectSystem\n{\n\t/// \u003Csummary\u003E Segundos entre pulsos: la variable Delay = 4.8 del original. \u003C/summary\u003E\n\tpublic const float DuracionPulso = 4.8f;\n\n\t/// \u003Csummary\u003E Silencio al cambiar de fase, como el wait 2 del original. \u003C/summary\u003E\n\tpublic const float SilencioCambioFase = 2f;\n\n\t/// \u003Csummary\u003E N\u00FAmero de pulso actual (0, 1, 2...); par = \u0022Loop 1\u0022, impar = \u0022Loop 2\u0022. -1 = todav\u00EDa no arranc\u00F3. \u003C/summary\u003E\n\tpublic int NumeroPulso { get; private set; } = -1;\n\n\t/// \u003Csummary\u003E Fase horror activa (espejo estable de RitmoConVars.FaseHorror). \u003C/summary\u003E\n\tpublic bool FaseHorror { get; private set; }\n\n\t/// \u003Csummary\u003E Se dispara en cada pulso con su n\u00FAmero. Los RitmoPersonaje se suscriben ac\u00E1. \u003C/summary\u003E\n\tpublic event Action\u003Cint\u003E AlPulso;\n\n\tfloat _proximoPulso;\n\tfloat _silencioHasta = -1f;\n\n\tpublic DirectorRitmo( Scene scene ) : base( scene )\n\t{\n\t\tListen( Stage.StartUpdate, 10, Latir, \u0022DirectorRitmoPersonaje\u0022 );\n\t\t_proximoPulso = Time.Now;\n\t}\n\n\t/// \u003Csummary\u003E Avance 0..1 dentro del pulso actual (para sincronizar bailes). \u003C/summary\u003E\n\tpublic float FraccionPulso =\u003E 1f - ((_proximoPulso - Time.Now) / DuracionPulso).Clamp( 0f, 1f );\n\n\tvoid Latir()\n\t{\n\t\t// Apagado: reloj detenido; al reactivar arranca limpio desde el pulso 0\n\t\tif ( !RitmoConVars.RitmoActivado )\n\t\t{\n\t\t\tNumeroPulso = -1;\n\t\t\t_proximoPulso = Time.Now;\n\t\t\treturn;\n\t\t}\n\n\t\t// Cambio de fase estilo \u0022Phase 2\u0022: corte, 2s de silencio y reloj a cero\n\t\tif ( RitmoConVars.FaseHorror != FaseHorror )\n\t\t{\n\t\t\tFaseHorror = RitmoConVars.FaseHorror;\n\t\t\tNumeroPulso = -1;\n\t\t\t_silencioHasta = Time.Now \u002B SilencioCambioFase;\n\t\t\t_proximoPulso = Time.Now \u002B SilencioCambioFase;\n\t\t}\n\n\t\tif ( Time.Now \u003C _silencioHasta ) return;\n\n\t\tif ( Time.Now \u003E= _proximoPulso )\n\t\t{\n\t\t\tNumeroPulso\u002B\u002B;\n\t\t\t_proximoPulso \u002B= DuracionPulso;\n\n\t\t\t// Tras una pausa larga (editor, hitch) no disparar r\u00E1fagas de pulsos\n\t\t\tif ( Time.Now \u003E= _proximoPulso )\n\t\t\t\t_proximoPulso = Time.Now \u002B DuracionPulso;\n\n\t\t\tAlPulso?.Invoke( NumeroPulso );\n\t\t}\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"HuesosAlSuelo.cs","FileName":"HuesosAlSuelo.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Planta huesos al suelo \u2014 el \u0022foot planting\u0022 de los gigantes que\n/// caminan con las manos o arrastran la cabeza (pedido para el Sea Eater).\n/// Cada frame toma la pose ANIMADA del hueso (patr\u00F3n BaileProcedural:\n/// TryGetBoneTransformAnimation, sin retroalimentaci\u00F3n), busca el piso con\n/// un rayo vertical y le clava la Z al terreno (offset \u002B suavizado).\n/// NO es IK de cadena (el codo no se dobla solo): es el truco de pegar el\n/// hueso terminal al suelo \u2014 para patas y cabezas que arrastran, rinde.\n/// OJO: comparte ClearBoneOverrides con los otros drivers de huesos\n/// (HuesosManipulados, BaileProcedural) \u2014 un solo driver por bot a la vez.\n/// \u003C/summary\u003E\npublic sealed class HuesosAlSuelo : Component\n{\n\t[Property, Description( \u0022Huesos a plantar, separados por coma (ej: forearm.L_phy, forearm.R_phy)\u0022 )]\n\tpublic string Huesos { get; set; } = \u0022\u0022;\n\n\t[Property, Description( \u0022Offset sobre el piso en unidades (0 = apoyado justo; subilo si el hueso es el centro de una mano gorda)\u0022 )]\n\tpublic float Altura { get; set; } = 0f;\n\n\t[Property, Description( \u0022Cu\u00E1nto sube el rayo por encima del hueso antes de buscar piso\u0022 )]\n\tpublic float AlturaBusqueda { get; set; } = 256f;\n\n\t[Property, Description( \u0022Cu\u00E1nto puede bajar el rayo buscando piso; si no hay piso en ese tramo el hueso queda animado\u0022 )]\n\tpublic float Alcance { get; set; } = 2048f;\n\n\t[Property, Description( \u0022Velocidad de acomodo (lerp exponencial); 0 = clavado duro\u0022 )]\n\tpublic float Suavizado { get; set; } = 10f;\n\n\tSkinnedModelRenderer _renderer;\n\treadonly Dictionary\u003Cstring, float\u003E _zSuave = new();\n\tbool _aplicado;\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_renderer = Components.Get\u003CSkinnedModelRenderer\u003E();\n\t\t_zSuave.Clear();\n\t}\n\n\tprotected override void OnDisabled() =\u003E Limpiar();\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !_renderer.IsValid() || _renderer.SceneModel == null ) return;\n\n\t\t// Muerto o ragdoll activo: la f\u00EDsica manda, nada de plantar huesos\n\t\t// (doble cerrojo: el tag puede llegar un frame tarde o vivir en el\n\t\t// root \u2014 y ModelPhysics encendido es ragdoll seguro)\n\t\tif ( GameObject.Tags.Has( \u0022sbh_muerto\u0022 ) || GameObject.Root.Tags.Has( \u0022sbh_muerto\u0022 )\n\t\t\t|| Components.Get\u003CModelPhysics\u003E() is not null )\n\t\t{\n\t\t\tLimpiar();\n\t\t\treturn;\n\t\t}\n\n\t\tvar sm = _renderer.SceneModel;\n\t\tsm.ClearBoneOverrides();\n\t\t_aplicado = false;\n\n\t\tforeach ( var crudo in (Huesos ?? \u0022\u0022).Split( \u0027,\u0027 ) )\n\t\t{\n\t\t\tvar nombre = crudo.Trim();\n\t\t\tif ( nombre == \u0022\u0022 ) continue;\n\n\t\t\tvar hueso = _renderer.Model?.Bones?.GetBone( nombre );\n\t\t\tif ( hueso == null ) continue;\n\t\t\tif ( !_renderer.TryGetBoneTransformAnimation( hueso, out var mundo ) ) continue;\n\n\t\t\t// Piso bajo el hueso, ignorando al propio bot y a los dem\u00E1s bichos\n\t\t\tvar piso = Scene.Trace\n\t\t\t\t.Ray( mundo.Position \u002B Vector3.Up * AlturaBusqueda,\n\t\t\t\t\t  mundo.Position \u002B Vector3.Down * Alcance )\n\t\t\t\t.IgnoreGameObjectHierarchy( GameObject.Root )\n\t\t\t\t.WithoutTags( \u0022player\u0022, \u0022sbh_nextbot\u0022 )\n\t\t\t\t.Run();\n\n\t\t\tif ( !piso.Hit ) continue;\n\n\t\t\tvar zObjetivo = piso.EndPosition.z \u002B Altura;\n\t\t\tif ( Suavizado \u003E 0f \u0026\u0026 _zSuave.TryGetValue( nombre, out var zPrevio ) )\n\t\t\t\tzObjetivo = MathX.Lerp( zPrevio, zObjetivo, Time.Delta * Suavizado );\n\t\t\t_zSuave[nombre] = zObjetivo;\n\n\t\t\tmundo.Position = mundo.Position.WithZ( zObjetivo );\n\t\t\tsm.SetBoneOverride( hueso.Index, sm.Transform.ToLocal( mundo ) );\n\t\t\t_aplicado = true;\n\t\t}\n\t}\n\n\tvoid Limpiar()\n\t{\n\t\tif ( !_aplicado ) return;\n\t\t_aplicado = false;\n\t\t_zSuave.Clear();\n\n\t\tif ( _renderer.IsValid() \u0026\u0026 _renderer.SceneModel != null )\n\t\t\t_renderer.SceneModel.ClearBoneOverrides();\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"HuesosManipulados.cs","FileName":"HuesosManipulados.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Huesos manipulados a mano (los ManipulateBone* de GMod, para el\n/// Advanced Bone Tool): guarda un ajuste por hueso (\u00E1ngulos/posici\u00F3n/escala,\n/// offsets en el MARCO LOCAL del hueso) y lo aplica cada frame POR ENCIMA de\n/// la animaci\u00F3n ya evaluada \u2014 patr\u00F3n BaileProcedural: TryGetBoneTransformAnimation\n/// lee la pose SIN overrides (sin retroalimentaci\u00F3n) y SetBoneOverride pisa.\n/// Con la lista vac\u00EDa no toca nada (la animaci\u00F3n queda pura). En un bot\n/// muerto (tag sbh_muerto) se apaga: el ragdoll manda.\n/// OJO: comparte SceneModel.ClearBoneOverrides con otros drivers de huesos\n/// (ej. BaileProcedural) \u2014 dos drivers vivos en el mismo bot se pisan entre s\u00ED.\n/// \u003C/summary\u003E\npublic sealed class HuesosManipulados : Component\n{\n\tpublic struct Ajuste\n\t{\n\t\tpublic Angles Angulos;\n\t\tpublic Vector3 Posicion;\n\t\tpublic float Escala;\n\n\t\tpublic static readonly Ajuste Identidad = new() { Escala = 1f };\n\n\t\tpublic bool EsIdentidad =\u003E\n\t\t\tAngulos == Angles.Zero \u0026\u0026 Posicion == Vector3.Zero \u0026\u0026 Escala == 1f;\n\t}\n\n\treadonly Dictionary\u003Cstring, Ajuste\u003E _ajustes = new();\n\tSkinnedModelRenderer _renderer;\n\tbool _aplicado;\n\n\tpublic IReadOnlyDictionary\u003Cstring, Ajuste\u003E Ajustes =\u003E _ajustes;\n\n\tpublic Ajuste De( string hueso )\n\t\t=\u003E hueso != null \u0026\u0026 _ajustes.TryGetValue( hueso, out var a ) ? a : Ajuste.Identidad;\n\n\tpublic void Fijar( string hueso, Ajuste ajuste )\n\t{\n\t\tif ( string.IsNullOrEmpty( hueso ) ) return;\n\n\t\tif ( ajuste.EsIdentidad )\n\t\t\t_ajustes.Remove( hueso );\n\t\telse\n\t\t\t_ajustes[hueso] = ajuste;\n\t}\n\n\tpublic void Reiniciar() =\u003E _ajustes.Clear();\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_renderer = Components.Get\u003CSkinnedModelRenderer\u003E();\n\t}\n\n\tprotected override void OnDisabled() =\u003E Limpiar();\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !_renderer.IsValid() || _renderer.SceneModel == null ) return;\n\n\t\tif ( _ajustes.Count == 0 || GameObject.Tags.Has( \u0022sbh_muerto\u0022 ) )\n\t\t{\n\t\t\tLimpiar();\n\t\t\treturn;\n\t\t}\n\n\t\tvar sm = _renderer.SceneModel;\n\t\tsm.ClearBoneOverrides();\n\n\t\tforeach ( var (nombre, ajuste) in _ajustes )\n\t\t{\n\t\t\tvar hueso = _renderer.Model?.Bones?.GetBone( nombre );\n\t\t\tif ( hueso == null ) continue;\n\n\t\t\t// Pose de la ANIMACI\u00D3N (mundo), sin nuestros overrides\n\t\t\tif ( !_renderer.TryGetBoneTransformAnimation( hueso, out var mundo ) ) continue;\n\n\t\t\tvar local = sm.Transform.ToLocal( mundo );\n\n\t\t\t// Offsets en el marco local del hueso (como ManipulateBone* de GMod)\n\t\t\tlocal.Position \u002B= local.Rotation * ajuste.Posicion;\n\t\t\tlocal.Rotation *= Rotation.From( ajuste.Angulos );\n\t\t\tlocal.Scale *= ajuste.Escala;\n\n\t\t\tsm.SetBoneOverride( hueso.Index, local );\n\t\t}\n\n\t\t_aplicado = true;\n\t}\n\n\tvoid Limpiar()\n\t{\n\t\tif ( !_aplicado ) return;\n\t\t_aplicado = false;\n\n\t\tif ( _renderer.IsValid() \u0026\u0026 _renderer.SceneModel != null )\n\t\t\t_renderer.SceneModel.ClearBoneOverrides();\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"LibroFusiones.cs","FileName":"LibroFusiones.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// [SBHBase] El libro de recetas de fusi\u00F3n. Junta DOS fuentes:\n///  - Las recetas de PACK: assets .fusion (RecetaFusion), solo lectura.\n///  - Las recetas PROPIAS: creadas en el juego con la tool Merge en modo\n///    crear; persisten como JSON en FileSystem.Data y sobreviven sesiones.\n/// La tool y el recetario consumen Todas() sin distinguir origen (salvo\n/// para permitir borrar: solo las propias).\n/// \u003C/summary\u003E\npublic static class LibroFusiones\n{\n\tconst string Archivo = \u0022sbh_fusiones.json\u0022;\n\n\t/// \u003Csummary\u003E Una receta lista para usar, venga del pack o del juego. \u003C/summary\u003E\n\tpublic class Receta\n\t{\n\t\tpublic List\u003CIngredienteFusion\u003E Ingredientes { get; set; } = new();\n\n\t\t/// \u003Csummary\u003E Ruta del prefab que aparece al fusionar. \u003C/summary\u003E\n\t\tpublic string RutaResultado { get; set; } = \u0022\u0022;\n\n\t\t/// \u003Csummary\u003E Ruta de un SoundEvent opcional. \u003C/summary\u003E\n\t\tpublic string RutaSonido { get; set; }\n\n\t\t/// \u003Csummary\u003E Vino de un asset .fusion: no se puede borrar desde el juego. \u003C/summary\u003E\n\t\tpublic bool DePack { get; set; }\n\t}\n\n\tstatic List\u003CReceta\u003E _propias;\n\n\t/// \u003Csummary\u003E Sube con cada alta/baja de receta propia \u2014 para BuildHash de la UI. \u003C/summary\u003E\n\tpublic static int Version { get; private set; }\n\n\t/// \u003Csummary\u003E Todas las recetas v\u00E1lidas: primero las del pack, despu\u00E9s las propias. \u003C/summary\u003E\n\tpublic static IEnumerable\u003CReceta\u003E Todas()\n\t{\n\t\tforeach ( var r in ResourceLibrary.GetAll\u003CRecetaFusion\u003E() )\n\t\t{\n\t\t\tif ( r.Resultado == null ) continue;\n\t\t\tif ( r.Ingredientes == null || r.Ingredientes.Count == 0 ) continue;\n\n\t\t\tyield return new Receta\n\t\t\t{\n\t\t\t\tIngredientes = r.Ingredientes,\n\t\t\t\tRutaResultado = r.Resultado.ResourcePath,\n\t\t\t\tRutaSonido = r.Sonido?.ResourcePath,\n\t\t\t\tDePack = true,\n\t\t\t};\n\t\t}\n\n\t\tforeach ( var r in Propias() )\n\t\t\tyield return r;\n\t}\n\n\tstatic List\u003CReceta\u003E Propias()\n\t{\n\t\tif ( _propias != null ) return _propias;\n\n\t\t_propias = new List\u003CReceta\u003E();\n\n\t\ttry\n\t\t{\n\t\t\tif ( FileSystem.Data.FileExists( Archivo ) )\n\t\t\t\t_propias = Json.Deserialize\u003CList\u003CReceta\u003E\u003E( FileSystem.Data.ReadAllText( Archivo ) ) ?? new();\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022LibroFusiones: no pude leer {Archivo} \u2014 {e.Message}\u0022 );\n\t\t}\n\n\t\tforeach ( var r in _propias )\n\t\t\tr.DePack = false;\n\n\t\treturn _propias;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Guarda una receta creada en el juego. Si ya hab\u00EDa una propia con los\n\t/// MISMOS ingredientes, se reemplaza \u2014 re-grabar una receta es la forma\n\t/// de cambiarle el resultado.\n\t/// \u003C/summary\u003E\n\tpublic static void Guardar( Receta receta )\n\t{\n\t\tvar propias = Propias();\n\t\tpropias.RemoveAll( r =\u003E MismosIngredientes( r, receta ) );\n\t\tpropias.Add( receta );\n\t\tPersistir();\n\t}\n\n\t/// \u003Csummary\u003E Borra una receta propia (las de pack no se tocan). \u003C/summary\u003E\n\tpublic static void Borrar( Receta receta )\n\t{\n\t\tif ( receta.DePack ) return;\n\n\t\tPropias().RemoveAll( r =\u003E MismosIngredientes( r, receta )\n\t\t\t\u0026\u0026 r.RutaResultado == receta.RutaResultado );\n\t\tPersistir();\n\t}\n\n\t/// \u003Csummary\u003E El bot\u00F3n nuclear: borra TODAS las recetas propias de un saque. \u003C/summary\u003E\n\tpublic static void BorrarTodas()\n\t{\n\t\tPropias().Clear();\n\t\tPersistir();\n\t}\n\n\tstatic void Persistir()\n\t{\n\t\ttry\n\t\t{\n\t\t\tFileSystem.Data.WriteAllText( Archivo, Json.Serialize( _propias ) );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022LibroFusiones: no pude guardar {Archivo} \u2014 {e.Message}\u0022 );\n\t\t}\n\n\t\tVersion\u002B\u002B;\n\t}\n\n\t// Mismo multiset de ingredientes (nombres sin may\u00FAsculas, cantidades sumadas)\n\tstatic bool MismosIngredientes( Receta a, Receta b )\n\t{\n\t\tstatic Dictionary\u003Cstring, int\u003E Mapa( Receta r ) =\u003E r.Ingredientes\n\t\t\t.GroupBy( i =\u003E i.Nombre?.Trim().ToLowerInvariant() ?? \u0022\u0022 )\n\t\t\t.ToDictionary( g =\u003E g.Key, g =\u003E g.Sum( i =\u003E Math.Max( 1, i.Cantidad ) ) );\n\n\t\tvar ma = Mapa( a );\n\t\tvar mb = Mapa( b );\n\n\t\treturn ma.Count == mb.Count\n\t\t\t\u0026\u0026 ma.All( kv =\u003E mb.TryGetValue( kv.Key, out var c ) \u0026\u0026 c == kv.Value );\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"RitmoPersonaje.cs","FileName":"RitmoPersonaje.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\n\n/// \u003Csummary\u003E\n/// [SBHBase] El ritmo de un sprunki: en CADA pulso del DirectorRitmo\n/// dispara su loop (variante A en \u0022Loop 1\u0022, B en \u0022Loop 2\u0022, como el juego\n/// original) si est\u00E1 \u0022en pantalla\u0022 (cerca de la c\u00E1mara) y vivo. Cada\n/// variante suena entonces cada 9.6s, intercaladas \u2014 el solape de sus colas\n/// es lo que hace el loop continuo (clave en los sets horror de ~7s).\n/// Re-disparar la MISMA variante corta su disparo anterior (sem\u00E1ntica de\n/// Scratch: \u0022start sound\u0022 reinicia el sonido), as\u00ED los audios que pasan de\n/// 9.6s (creepy, better_windup...) se recortan solos como en el original.\n/// No depende de SBHBase: detecta la muerte por la presencia del ragdoll\n/// (ModelPhysics o el Rigidbody del plan B de MuerteNextbot).\n/// \u003C/summary\u003E\npublic sealed class RitmoPersonaje : Component\n{\n\t[Property, Description( \u0022Loop normal, variante A (pulsos \u0027Loop 1\u0027). Vac\u00EDo = personaje mudo en fase normal (como Black en el original)\u0022 )]\n\tpublic SoundEvent LoopNormalA { get; set; }\n\n\t[Property, Description( \u0022Loop normal, variante B (pulsos \u0027Loop 2\u0027). Vac\u00EDo = repite la A\u0022 )]\n\tpublic SoundEvent LoopNormalB { get; set; }\n\n\t[Property, Description( \u0022Loop de la fase horror, variante A. Vac\u00EDo = sigue con el normal\u0022 )]\n\tpublic SoundEvent LoopHorrorA { get; set; }\n\n\t[Property, Description( \u0022Loop de la fase horror, variante B. Vac\u00EDo = repite la A\u0022 )]\n\tpublic SoundEvent LoopHorrorB { get; set; }\n\n\t[Property, Description( \u0022Distancia m\u00E1xima a la c\u00E1mara para que dispare su ritmo \u2014 el \u0027estar en pantalla\u0027\u0022 )]\n\tpublic float DistanciaAudible { get; set; } = 3000f;\n\n\t[Property, Description( \u0022Segundos del fade de silencio al morir (0 = corte seco)\u0022 )]\n\tpublic float FadeMuerte { get; set; } = 0.35f;\n\n\tDirectorRitmo _director;\n\n\t// Un handle por variante (0 = A/pulsos pares, 1 = B/impares): igual que los\n\t// pares de sonidos del original (bass3/bass4, amen3/amen4...), cada slot se\n\t// reinicia a s\u00ED mismo cada 9.6s sin cortar al otro\n\treadonly SoundHandle[] _handles = new SoundHandle[2];\n\treadonly float[] _volumenes = { 1f, 1f }; // volumen al disparar (respeta el del .sound)\n\tfloat _fade;              // avance del fade de muerte\n\tbool _muriendo;\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_director = Scene.GetSystem\u003CDirectorRitmo\u003E();\n\t\tif ( _director != null )\n\t\t\t_director.AlPulso \u002B= EnPulso;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( _director != null )\n\t\t\t_director.AlPulso -= EnPulso;\n\n\t\tfor ( var i = 0; i \u003C _handles.Length; i\u002B\u002B )\n\t\t{\n\t\t\t_handles[i]?.Stop();\n\t\t\t_handles[i] = null;\n\t\t}\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\t// Los loops viajan con el personaje (si no, quedan sonando donde arrancaron)\n\t\tfor ( var i = 0; i \u003C _handles.Length; i\u002B\u002B )\n\t\t{\n\t\t\tif ( _handles[i] != null \u0026\u0026 _handles[i].IsValid() )\n\t\t\t\t_handles[i].Position = WorldPosition;\n\t\t}\n\n\t\t// Muerte: el ritmo se apaga YA, con un fade cortito para que no haga\n\t\t// click \u2014 la cola musical no debe sobrevivir al ragdoll\n\t\tvar muerto = EstaMuerto();\n\n\t\tif ( muerto \u0026\u0026 !_muriendo )\n\t\t{\n\t\t\t_muriendo = true;\n\t\t\t_fade = 0f;\n\t\t}\n\t\telse if ( !muerto )\n\t\t{\n\t\t\t_muriendo = false; // por si algo lo revive: vuelve a sonar\n\t\t}\n\n\t\tif ( _muriendo )\n\t\t{\n\t\t\t_fade \u002B= Time.Delta;\n\t\t\tvar restante = FadeMuerte \u003E 0f ? 1f - (_fade / FadeMuerte) : 0f;\n\n\t\t\tfor ( var i = 0; i \u003C _handles.Length; i\u002B\u002B )\n\t\t\t{\n\t\t\t\tif ( _handles[i] == null || !_handles[i].IsValid() ) continue;\n\n\t\t\t\tif ( restante \u003C= 0f )\n\t\t\t\t{\n\t\t\t\t\t_handles[i].Stop();\n\t\t\t\t\t_handles[i] = null;\n\t\t\t\t}\n\t\t\t\telse\n\t\t\t\t{\n\t\t\t\t\t_handles[i].Volume = _volumenes[i] * restante;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tvoid EnPulso( int numero )\n\t{\n\t\tif ( !PuedeSonar() ) return;\n\n\t\tvar horror = _director.FaseHorror \u0026\u0026 (LoopHorrorA != null || LoopHorrorB != null);\n\n\t\t// Cada pulso dispara: A en los pares (\u0022Loop 1\u0022), B en los impares\n\t\t// (\u0022Loop 2\u0022). Con B vac\u00EDa, el mismo sonido ocupa los dos slots \u2014 igual\n\t\t// que los pares duplicados del original (amen3/amen4, kick/kick2)\n\t\tvar esA = numero % 2 == 0;\n\n\t\tvar sonido = Elegir( horror, esA );\n\t\tif ( sonido == null ) return;\n\n\t\tvar slot = esA ? 0 : 1;\n\n\t\t// Re-disparar la variante REINICIA su disparo anterior (sem\u00E1ntica\n\t\t// Scratch); la cola de la OTRA variante sigue sonando \u2014 ese solape\n\t\t// intercalado es el loop continuo del original\n\t\t_handles[slot]?.Stop();\n\t\t_handles[slot] = Sound.Play( sonido, WorldPosition );\n\n\t\tif ( _handles[slot] != null \u0026\u0026 _handles[slot].IsValid() )\n\t\t\t_volumenes[slot] = _handles[slot].Volume;\n\n\t\t// Aviso gen\u00E9rico \u0022estoy cantando este pulso\u0022 (lo consume el puente\n\t\t// del juego para la cara de canto de las expresiones)\n\t\tAlCantar?.Invoke( DirectorRitmo.DuracionPulso );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Disparado en cada retrigger del loop, con los segundos que cubre hasta\n\t/// el pr\u00F3ximo (un pulso, 4.8). Quien quiera animar el canto se cuelga ac\u00E1.\n\t/// \u003C/summary\u003E\n\tpublic event System.Action\u003Cfloat\u003E AlCantar;\n\n\tSoundEvent Elegir( bool horror, bool esA )\n\t{\n\t\tif ( horror )\n\t\t\treturn esA ? (LoopHorrorA ?? LoopHorrorB) : (LoopHorrorB ?? LoopHorrorA);\n\n\t\treturn esA ? (LoopNormalA ?? LoopNormalB) : (LoopNormalB ?? LoopNormalA);\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Cad\u00E1ver = tag \u0022sbh_muerto\u0022 (lo pone MuerteNextbot) o ragdoll activo.\n\t/// OJO: no mirar Rigidbody \u2014 un bot alzado por la physgun (AgarreNextbot)\n\t/// lleva Rigidbody temporal y est\u00E1 bien vivo: que siga sonando en el aire.\n\t/// \u003C/summary\u003E\n\tbool EstaMuerto()\n\t{\n\t\tif ( GameObject.Tags.Has( \u0022sbh_muerto\u0022 ) ) return true;\n\t\tif ( Components.Get\u003CModelPhysics\u003E().IsValid() ) return true;\n\t\treturn false;\n\t}\n\n\tbool PuedeSonar()\n\t{\n\t\tif ( EstaMuerto() ) return false;\n\n\t\t// \u0022En pantalla\u0022: cerca de la c\u00E1mara (el falloff del .sound hace el resto)\n\t\tvar cam = Scene.Camera;\n\t\tif ( cam.IsValid() \u0026\u0026 cam.WorldPosition.Distance( WorldPosition ) \u003E DistanciaAudible )\n\t\t\treturn false;\n\n\t\treturn true;\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"Code/EventosAnim.cs","FileName":"EventosAnim.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// [SBHBase] EVENTOS DE ANIMACI\u00D3N \u2014 los animevents de GMod (Impact,\n/// ImpactKill, sonidos...) como datos editables en vivo: por personaje, un\n/// mapa anim \u2192 eventos en FRAMES a 30fps (el idioma de Blender y del QC).\n///\n/// Es el hermano del sistema de partituras de caras (PartituraStore), pero\n/// la l\u00EDnea de tiempo es cada ANIMACI\u00D3N en vez del pulso musical. Capas de\n/// origen (mismo dise\u00F1o): JSON local de FileSystem.Data (lo pinta el editor\n/// en vivo, o Notepad mientras tanto) \u2192 de f\u00E1brica horneada en la librer\u00EDa\n/// (EventosDeFabrica, tools/hornear_eventos.bat) \u2192 fallback del prefab\n/// (RetardoImpacto cl\u00E1sico: quien no tiene eventos no cambia en nada).\n///\n/// Consumidores hoy: NextbotBrain (Impact de CADA variante del pool de\n/// ataques \u2014 antes un solo RetardoImpacto desincronizaba a 3 de 4 golpes) y\n/// SbhJumpscareCine (ImpactKill de execution). Ma\u00F1ana: pasos, efectos, y el\n/// editor visual estilo Beat.\n/// \u003C/summary\u003E\npublic static class EventosAnim\n{\n\tpublic class Evento\n\t{\n\t\tpublic string Anim { get; set; }\n\t\tpublic int Frame { get; set; }\n\t\tpublic string Nombre { get; set; }\n\n\t\t/// \u003Csummary\u003E Par\u00E1metro opcional del evento: la ruta del .sound en Sound/Step; a futuro \u0022prefab@hueso\u0022 en Efecto. \u003C/summary\u003E\n\t\tpublic string Dato { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Capa visual del editor (0 = la primera). Eventos simult\u00E1neos viven\n\t\t/// en capas distintas para verse todos; el DESPACHADOR la ignora \u2014 se\n\t\t/// disparan todas las capas por igual. JSON viejos: sin el campo, 0.\n\t\t/// \u003C/summary\u003E\n\t\tpublic int Capa { get; set; }\n\t}\n\n\tpublic class Datos\n\t{\n\t\tpublic List\u003CEvento\u003E Eventos { get; set; } = new();\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Head-tracking APAGADO para este personaje (2026-08-10): en rigs de\n\t\t/// GMod donde \u0022head\u0022 es una parte especial \u2014 la tapa del libro de\n\t\t/// Buuik, la tapa del pastel de Cake \u2014 la mirada la retuerce feo.\n\t\t/// Lo togglea el editor de Anim Events y viaja con el horneado.\n\t\t/// \u003C/summary\u003E\n\t\tpublic bool SinCabeza { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Fuerza del salto EN POSESI\u00D3N de este personaje (2026-08-10).\n\t\t/// 0 = la que trae su prefab (SaltoNextbot.FuerzaSaltoPosesion, que\n\t\t/// escribe el addon de Blender). Se afina desde el editor Anim Events\n\t\t/// y viaja horneada, como todo lo dem\u00E1s.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float FuerzaSalto { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Velocidad de CAZA afinada (2026-08-10, \u0022el Siren patina\u0022): unidades\n\t\t/// por segundo al perseguir y al correr pose\u00EDdo. 0 = la del prefab\n\t\t/// (NavMeshAgent.MaxSpeed, que escribe el addon). Editor \u2192 JSON \u2192\n\t\t/// horneado, la tripleta de siempre.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float Velocidad { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// VelocidadAnimacion afinada (2026-08-10): a cu\u00E1ntas unidades/seg de\n\t\t/// suelo corresponde la anim de marcha a ritmo 1 \u2014 LA perilla del\n\t\t/// patinaje. 0 = la del prefab.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float VelocidadAnim { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Multiplicador de velocidad POR ANIMACI\u00D3N (2026-08-10, la feature\n\t\t/// agendada): anim (min\u00FAsculas) \u2192 mult. 1.5 = \u0022el run de este bicho\n\t\t/// va 50% m\u00E1s r\u00E1pido, para siempre\u0022. Ausente = 1 (natural). Gana\n\t\t/// sobre el tope RitmoAnimMax y sobre la regla \u0022ataque nunca\n\t\t/// acelerado\u0022: es la palabra expl\u00EDcita del artista.\n\t\t/// \u003C/summary\u003E\n\t\tpublic Dictionary\u003Cstring, float\u003E RitmosAnim { get; set; } = new();\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Vida m\u00E1xima afinada (2026-08-12, pedido de la primera rese\u00F1a del\n\t\t/// juego): 0 = la del prefab (Vida.VidaMaxima). Se aplica al SPAWN \u2014\n\t\t/// los bots ya paridos conservan la suya. Editor \u2192 JSON \u2192 horneado.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float Vida { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Da\u00F1o por golpe afinado (2026-08-12): 0 = el del prefab\n\t\t/// (NextbotBrain.Danio). Se aplica al spawn, como la vida.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float Danio { get; set; }\n\n\t\t/// \u003Csummary\u003E\n\t\t/// Super da\u00F1o afinado (2026-08-12, el \u0022Super Damage\u0022 de DRGBase):\n\t\t/// da\u00F1o alternativo que sale con ProbDanioSuper. 0 = el del prefab.\n\t\t/// \u003C/summary\u003E\n\t\tpublic float DanioSuper { get; set; }\n\n\t\t/// \u003Csummary\u003E Probabilidad del super da\u00F1o en % por golpe (0 = la del prefab). \u003C/summary\u003E\n\t\tpublic float ProbDanioSuper { get; set; }\n\n\t\t/// \u003Csummary\u003E Regeneraci\u00F3n pasiva afinada, HP/seg (0 = la del prefab). \u003C/summary\u003E\n\t\tpublic float Regeneracion { get; set; }\n\n\t\t/// \u003Csummary\u003E Cooldown entre ataques afinado, en segundos (0 = el del prefab). \u003C/summary\u003E\n\t\tpublic float CooldownAtaque { get; set; }\n\t}\n\n\t/// \u003Csummary\u003E \u00BFEste personaje tiene el head-tracking apagado por datos? \u003C/summary\u003E\n\tpublic static bool CabezaApagada( string personaje )\n\t\t=\u003E !string.IsNullOrEmpty( personaje ) \u0026\u0026 Cargar( personaje ).SinCabeza;\n\n\t/// \u003Csummary\u003E Fuerza de salto afinada para este personaje, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float FuerzaSaltoDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).FuerzaSalto;\n\n\t/// \u003Csummary\u003E Velocidad de caza afinada para este personaje, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float VelocidadDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).Velocidad;\n\n\t/// \u003Csummary\u003E VelocidadAnimacion afinada para este personaje, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float VelocidadAnimDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).VelocidadAnim;\n\n\t/// \u003Csummary\u003E Vida m\u00E1xima afinada para este personaje, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float VidaDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).Vida;\n\n\t/// \u003Csummary\u003E Da\u00F1o por golpe afinado para este personaje, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float DanioDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).Danio;\n\n\t/// \u003Csummary\u003E Super da\u00F1o afinado para este personaje, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float DanioSuperDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).DanioSuper;\n\n\t/// \u003Csummary\u003E Probabilidad (%) del super da\u00F1o afinada, o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float ProbDanioSuperDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).ProbDanioSuper;\n\n\t/// \u003Csummary\u003E Regeneraci\u00F3n pasiva afinada (HP/seg), o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float RegeneracionDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).Regeneracion;\n\n\t/// \u003Csummary\u003E Cooldown de ataque afinado (seg), o 0 si manda el prefab. \u003C/summary\u003E\n\tpublic static float CooldownDe( string personaje )\n\t\t=\u003E string.IsNullOrEmpty( personaje ) ? 0f : Cargar( personaje ).CooldownAtaque;\n\n\t/// \u003Csummary\u003E Multiplicador de velocidad de UNA anim de este personaje (1 = natural). \u003C/summary\u003E\n\tpublic static float RitmoAnimacionDe( string personaje, string anim )\n\t{\n\t\tif ( string.IsNullOrEmpty( personaje ) || string.IsNullOrEmpty( anim ) ) return 1f;\n\n\t\tvar ritmos = Cargar( personaje ).RitmosAnim;\n\t\tif ( ritmos == null || ritmos.Count == 0 ) return 1f;\n\n\t\treturn (ritmos.TryGetValue( anim, out var m ) || ritmos.TryGetValue( anim.ToLowerInvariant(), out m ))\n\t\t\t\u0026\u0026 m \u003E 0f ? m : 1f;\n\t}\n\n\tconst string Carpeta = \u0022sbh_animevents\u0022;\n\tconst float Fps = 30f;\n\n\tstatic readonly Dictionary\u003Cstring, Datos\u003E _cache = new();\n\n\t/// \u003Csummary\u003E El personaje seg\u00FAn la convenci\u00F3n de todo el proyecto: el nombre del vmdl. \u003C/summary\u003E\n\tpublic static string PersonajeDe( SkinnedModelRenderer renderer )\n\t\t=\u003E System.IO.Path.GetFileNameWithoutExtension( renderer?.Model?.ResourcePath ?? \u0022\u0022 );\n\n\tstatic string Ruta( string personaje ) =\u003E $\u0022{Carpeta}/{personaje}.json\u0022;\n\n\tpublic static Datos Cargar( string personaje )\n\t{\n\t\tif ( string.IsNullOrEmpty( personaje ) ) return new Datos();\n\t\tif ( _cache.TryGetValue( personaje, out var datos ) ) return datos;\n\n\t\tdatos = FileSystem.Data.FileExists( Ruta( personaje ) )\n\t\t\t? FileSystem.Data.ReadJson\u003CDatos\u003E( Ruta( personaje ) )\n\t\t\t: null;\n\n\t\t// Sin JSON local mandan los eventos DE F\u00C1BRICA horneados en la\n\t\t// librer\u00EDa (tools/hornear_eventos.bat): lo pintado ac\u00E1 viaja al\n\t\t// publicar y al equipo, igual que las partituras.\n\t\tdatos ??= EventosDeFabrica.Obtener( personaje ) ?? new Datos();\n\n\t\t_cache[personaje] = datos;\n\t\treturn datos;\n\t}\n\n\tpublic static void Guardar( string personaje, Datos datos )\n\t{\n\t\tFileSystem.Data.CreateDirectory( Carpeta );\n\t\tFileSystem.Data.WriteJson( Ruta( personaje ), datos );\n\t\t_cache[personaje] = datos;\n\t}\n\n\t/// \u003Csummary\u003E El editor (o un reload a mano) invalida y los bots releen al pr\u00F3ximo golpe. \u003C/summary\u003E\n\tpublic static void Invalidar( string personaje = null )\n\t{\n\t\tif ( personaje == null ) _cache.Clear();\n\t\telse _cache.Remove( personaje );\n\t}\n\n\t/// \u003Csummary\u003E \u00BFEsta anim tiene pintado al menos un evento de este nombre? \u003C/summary\u003E\n\tpublic static bool TieneEvento( string personaje, string anim, string evento )\n\t{\n\t\tif ( string.IsNullOrEmpty( anim ) || string.IsNullOrEmpty( evento ) ) return false;\n\n\t\treturn Cargar( personaje ).Eventos.Any( e =\u003E\n\t\t\tstring.Equals( e.Anim, anim, System.StringComparison.OrdinalIgnoreCase ) \u0026\u0026\n\t\t\tstring.Equals( e.Nombre, evento, System.StringComparison.OrdinalIgnoreCase ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Segundos (a ritmo 1) del evento pedido en esa anim, o null si no est\u00E1\n\t/// pintado \u2014 el consumidor cae a su fallback cl\u00E1sico.\n\t/// \u003C/summary\u003E\n\tpublic static float? TiempoDe( string personaje, string anim, string evento )\n\t{\n\t\tif ( string.IsNullOrEmpty( anim ) || string.IsNullOrEmpty( evento ) ) return null;\n\n\t\tvar ev = Cargar( personaje ).Eventos.FirstOrDefault( e =\u003E\n\t\t\tstring.Equals( e.Anim, anim, System.StringComparison.OrdinalIgnoreCase ) \u0026\u0026\n\t\t\tstring.Equals( e.Nombre, evento, System.StringComparison.OrdinalIgnoreCase ) );\n\n\t\treturn ev != null ? ev.Frame / Fps : null;\n\t}\n\n\t[ConCmd( \u0022sbh_eventos_recargar\u0022, Help = \u0022Relee los JSON de eventos de animaci\u00F3n (sbh_animevents/) \u2014 para editar con Notepad sin reiniciar.\u0022 )]\n\tpublic static void Recargar()\n\t{\n\t\tInvalidar();\n\t\tLog.Info( \u0022\uD83C\uDF9E EventosAnim: cach\u00E9 limpiada, los JSON se releen al pr\u00F3ximo uso\u0022 );\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"Code/PartiturasDeFabrica.cs","FileName":"PartiturasDeFabrica.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\r\n\r\nusing System.Collections.Generic;\r\n\r\n/// \u003Csummary\u003E\r\n/// [SBHBase] Partituras de fabrica - GENERADO por tools/hornear_partituras.bat\r\n/// (2026-08-05 21:22) a partir de lo pintado en el editor Beat. NO editar a mano: pintar\r\n/// en el juego y volver a hornear. Viajan con la libreria (publicacion y equipo);\r\n/// el JSON local del jugador, si existe, las pisa (PartituraStore.Cargar).\r\n/// Pistas por personaje: LoopA, LoopB, HorrorA, HorrorB (48 casilleros, -1 = vacio).\r\n/// \u003C/summary\u003E\r\npublic static class PartiturasDeFabrica\r\n{\r\n\tstatic readonly Dictionary\u003Cstring, int[][]\u003E Tabla = new()\r\n\t{\r\n\t\t[\u0022sprunki_hrr_oren\u0022] = new[]\r\n\t\t{\r\n\t\t\tnew[] { 0, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t},\r\n\t\t[\u0022sprunki_jevin\u0022] = new[]\r\n\t\t{\r\n\t\t\tnew[] { 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, 3, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { 0, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t},\r\n\t\t[\u0022sprunki_mrfuncomputer\u0022] = new[]\r\n\t\t{\r\n\t\t\tnew[] { 2, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, 3, -1, -1, -1, -1, -1, -1, 4, -1, -1, -1, -1, 5, -1, -1, -1, -1, -1, 6, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 7, -1 },\r\n\t\t\tnew[] { 3, -1, -1, -1, -1, 4, -1, -1, -1, -1, -1, 5, -1, -1, -1, -1, -1, 6, -1, -1, -1, -1, -1, 7, -1, -1, -1, -1, -1, 10, -1, -1, -1, -1, -1, 9, -1, -1, -1, -1, -1, 10, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t},\r\n\t\t[\u0022sprunki_oren\u0022] = new[]\r\n\t\t{\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t},\r\n\t\t[\u0022sprunki_owakcx\u0022] = new[]\r\n\t\t{\r\n\t\t\tnew[] { 2, -1, -1, 0, -1, -1, 2, -1, 0, -1, -1, 2, -1, -1, -1, -1, -1, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t\tnew[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 },\r\n\t\t},\r\n\t};\r\n\r\n\tpublic static PartituraStore.Datos Obtener( string personaje )\r\n\t{\r\n\t\tif ( string.IsNullOrEmpty( personaje ) ) return null;\r\n\t\tif ( !Tabla.TryGetValue( personaje, out var pistas ) ) return null;\r\n\r\n\t\treturn new PartituraStore.Datos\r\n\t\t{\r\n\t\t\tLoopA = new List\u003Cint\u003E( pistas[0] ),\r\n\t\t\tLoopB = new List\u003Cint\u003E( pistas[1] ),\r\n\t\t\tHorrorA = new List\u003Cint\u003E( pistas[2] ),\r\n\t\t\tHorrorB = new List\u003Cint\u003E( pistas[3] ),\r\n\t\t};\r\n\t}\r\n}\r\n"},{"Ident":"ksbravo.sbhbase","Path":"Code/RespawnAlMorir.cs","FileName":"RespawnAlMorir.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// Al morir la Vida de este GameObject, lo teletransporta a un\n/// SpawnPoint aleatorio y lo revive. Para jugadores.\n/// \u003C/summary\u003E\npublic sealed class RespawnAlMorir : Component\n{\n\t[RequireComponent] Vida Vida { get; set; }\n\n\t[Property, Description( \u0022Sonido al morir (opcional)\u0022 )]\n\tpublic SoundEvent SonidoMuerte { get; set; }\n\n\tprotected override void OnEnabled()\n\t{\n\t\tVida.AlMorir \u002B= Respawnear;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tVida.AlMorir -= Respawnear;\n\t}\n\n\tvoid Respawnear()\n\t{\n\t\tif ( SonidoMuerte != null )\n\t\t\tSound.Play( SonidoMuerte, WorldPosition );\n\n\t\tvar spawns = Scene.GetAllComponents\u003CSpawnPoint\u003E().ToList();\n\t\tif ( spawns.Count \u003E 0 )\n\t\t{\n\t\t\tvar spawn = Game.Random.FromList( spawns );\n\t\t\tWorldPosition = spawn.WorldPosition;\n\t\t}\n\n\t\tVida.Revivir();\n\t\tLog.Info( \u0022\uD83D\uDC80 Jugador muerto \u2192 respawn con vida completa\u0022 );\n\t}\n}"},{"Ident":"ksbravo.sbhbase","Path":"Code/TemblorCamara.cs","FileName":"TemblorCamara.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\n\n/// \u003Csummary\u003E\n/// [SBHBase] TEMBLOR DE C\u00C1MARA gen\u00E9rico (2026-08-10, pedido de H\u00E9ctor para\n/// los pisotones del Siren Head). Modelo de \u0022trauma\u0022: los golpes suman\n/// trauma, el trauma decae solo, y mientras haya trauma la vista vibra\n/// (offset \u002B roll aleatorios, curva cuadr\u00E1tica).\n///\n/// Es un ICameraModifier con el ORDEN M\u00C1S ALTO: corre al final de la cadena\n/// de c\u00E1mara del engine, sobre la vista YA calculada del frame \u2014 primera\n/// persona, posesi\u00F3n (\u00F3rbita), cine \u2014 as\u00ED que no hay realimentaci\u00F3n posible\n/// (la versi\u00F3n anterior mutaba la c\u00E1mara post-update y la \u00F3rbita de la\n/// posesi\u00F3n heredaba la sacudida: \u0022se buguea la c\u00E1mara\u0022, 2026-08-10).\n///\n/// Cualquier sistema puede llamar \u003Csee cref=\u0022Sacudir\u0022/\u003E: pisadas de gigante,\n/// explosiones, el tr\u00EDpode cuando vuelva del taller...\n/// \u003C/summary\u003E\npublic sealed class TemblorCamara : Component, ICameraModifier\n{\n\tstatic TemblorCamara _instancia;\n\tfloat _trauma;\n\n\t/// \u003Csummary\u003E Suma un golpe de temblor (0.1 = apenas, 1 = terremoto). \u003C/summary\u003E\n\tpublic static void Sacudir( Scene scene, float intensidad )\n\t{\n\t\tif ( intensidad \u003C= 0f ) return;\n\t\tvar t = Asegurar( scene );\n\t\tif ( t.IsValid() )\n\t\t\tt._trauma = ( t._trauma \u002B intensidad ).Clamp( 0f, 1.5f );\n\t}\n\n\t// Patr\u00F3n FadeHorror.Asegurar: un solo GameObject de servicio en la escena\n\tstatic TemblorCamara Asegurar( Scene scene )\n\t{\n\t\tif ( _instancia.IsValid() ) return _instancia;\n\t\tif ( scene == null ) return null;\n\n\t\tvar go = scene.CreateObject();\n\t\tgo.Name = \u0022sbh_temblor_camara\u0022;\n\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;\n\t\t_instancia = go.Components.Create\u003CTemblorCamara\u003E();\n\t\treturn _instancia;\n\t}\n\n\tint ICameraModifier.CameraOrder =\u003E 9999; // el \u00FAltimo de la cadena\n\n\tvoid ICameraModifier.ModifyCamera( CameraComponent camera, ref CameraView view )\n\t{\n\t\tif ( _trauma \u003C= 0.001f ) { _trauma = 0f; return; }\n\n\t\tvar fuerza = _trauma * _trauma;\n\t\tview.Position \u002B= Vector3.Random * ( 6f * fuerza );\n\t\tview.Rotation *= Rotation.From(\n\t\t\tGame.Random.Float( -1.2f, 1.2f ) * fuerza,\n\t\t\tGame.Random.Float( -1.2f, 1.2f ) * fuerza,\n\t\t\tGame.Random.Float( -2.5f, 2.5f ) * fuerza );\n\n\t\t// Decae solo: un golpe de 0.4 dura ~un tercio de segundo\n\t\t_trauma = System.MathF.Max( 0f, _trauma - Time.Delta * 2.2f );\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"EfectosNextbot.cs","FileName":"EfectosNextbot.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Efectos de part\u00EDculas del nextbot: al aparecer, al morir, al\n/// recibir da\u00F1o y cuando su golpe conecta. Cada efecto es un prefab one-shot\n/// (emisor con Loop apagado y DestroyOnEnd encendido) que se clona en el\n/// lugar y se limpia solo cuando las part\u00EDculas terminan \u2014 mismo patr\u00F3n que\n/// los efectos del rollermine de sandbox. Cualquier campo vac\u00EDo simplemente\n/// no emite nada.\n/// Requiere Vida en el mismo GameObject para muerte/da\u00F1o y NextbotBrain\n/// para el golpe conectado; sin ellos, esos efectos quedan mudos.\n/// \u003C/summary\u003E\npublic sealed class EfectosNextbot : Component\n{\n\t[Property, Description( \u0022Prefab de part\u00EDculas al aparecer (se clona en los pies)\u0022 )]\n\tpublic GameObject EfectoSpawn { get; set; }\n\n\t[Property, Description( \u0022Prefab de part\u00EDculas al morir (se clona en el torso)\u0022 )]\n\tpublic GameObject EfectoMuerte { get; set; }\n\n\t[Property, Description( \u0022Prefab de part\u00EDculas al recibir da\u00F1o, tipo sangre (en el torso; el golpe mortal usa solo el de muerte)\u0022 )]\n\tpublic GameObject EfectoDanio { get; set; }\n\n\t[Property, Description( \u0022Prefab de part\u00EDculas cuando SU golpe conecta (se clona en la v\u00EDctima)\u0022 )]\n\tpublic GameObject EfectoGolpe { get; set; }\n\n\t[Property, Description( \u0022Altura del torso sobre los pies, donde nacen los efectos de da\u00F1o y muerte\u0022 )]\n\tpublic float AlturaTorso { get; set; } = 40f;\n\n\tVida _vida;\n\tNextbotBrain _cerebro;\n\n\tVector3 Torso =\u003E WorldPosition \u002B Vector3.Up * AlturaTorso;\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_vida = Components.Get\u003CVida\u003E();\n\t\t_cerebro = Components.Get\u003CNextbotBrain\u003E( true );\n\n\t\tif ( _vida.IsValid() )\n\t\t{\n\t\t\t_vida.AlRecibirDanio \u002B= AlDaniado;\n\t\t\t_vida.AlMorir \u002B= AlMorirBot;\n\t\t}\n\n\t\tif ( _cerebro.IsValid() )\n\t\t\t_cerebro.AlGolpear \u002B= AlConectarGolpe;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( _vida.IsValid() )\n\t\t{\n\t\t\t_vida.AlRecibirDanio -= AlDaniado;\n\t\t\t_vida.AlMorir -= AlMorirBot;\n\t\t}\n\n\t\tif ( _cerebro.IsValid() )\n\t\t\t_cerebro.AlGolpear -= AlConectarGolpe;\n\t}\n\n\tprotected override void OnStart()\n\t{\n\t\tClonar( EfectoSpawn, WorldPosition );\n\t}\n\n\tvoid AlDaniado( float cantidad )\n\t{\n\t\t// El golpe mortal ya tiene su propio efecto (AlMorir): no duplicar\n\t\tif ( _vida.IsValid() \u0026\u0026 _vida.EstaMuerto ) return;\n\n\t\tClonar( EfectoDanio, Torso );\n\t}\n\n\tvoid AlMorirBot()\n\t{\n\t\tClonar( EfectoMuerte, Torso );\n\t}\n\n\tvoid AlConectarGolpe( IVida victima, Vector3 punto )\n\t{\n\t\tClonar( EfectoGolpe, punto );\n\t}\n\n\tstatic void Clonar( GameObject prefab, Vector3 posicion )\n\t{\n\t\tif ( prefab == null || !prefab.IsValid() ) return;\n\n\t\tvar go = prefab.Clone( posicion );\n\t\tif ( !go.IsValid() ) return;\n\n\t\t// El DestroyOnEnd del emisor limpia las capas de part\u00EDculas, pero el\n\t\t// objeto ra\u00EDz del prefab quedar\u00EDa hu\u00E9rfano en la escena para siempre\n\t\t// (TemporaryEffect: el mismo componente del engine que usan los gibs)\n\t\tgo.Components.Create\u003CTemporaryEffect\u003E().DestroyAfterSeconds = 5f;\n\t}\n}\n\n"},{"Ident":"ksbravo.sbhbase","Path":"PartituraStore.cs","FileName":"PartituraStore.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Guardado de partituras de caras, una por personaje, en\n/// FileSystem.Data (sprunki_partituras/\u0026lt;personaje\u0026gt;.json). El editor del\n/// men\u00FA C guarda ac\u00E1 en cada pincelada; PartituraInyector carga al inyectar.\n/// \u003C/summary\u003E\npublic static class PartituraStore\n{\n\tpublic class Datos\n\t{\n\t\tpublic List\u003Cint\u003E LoopA { get; set; } = PartituraCaras.PistaVacia();\n\t\tpublic List\u003Cint\u003E LoopB { get; set; } = PartituraCaras.PistaVacia();\n\t\tpublic List\u003Cint\u003E HorrorA { get; set; } = PartituraCaras.PistaVacia();\n\t\tpublic List\u003Cint\u003E HorrorB { get; set; } = PartituraCaras.PistaVacia();\n\t}\n\n\tconst string Carpeta = \u0022sprunki_partituras\u0022;\n\n\tstatic string Ruta( string personaje ) =\u003E $\u0022{Carpeta}/{personaje}.json\u0022;\n\n\tpublic static Datos Cargar( string personaje )\n\t{\n\t\tif ( string.IsNullOrEmpty( personaje ) ) return null;\n\n\t\t// El JSON local (lo \u00FAltimo pintado en el editor) manda; sin \u00E9l, las\n\t\t// partituras de f\u00E1brica horneadas en la librer\u00EDa (tools/hornear_partituras)\n\t\tif ( !FileSystem.Data.FileExists( Ruta( personaje ) ) )\n\t\t\treturn DeFabrica( personaje );\n\n\t\tvar datos = FileSystem.Data.ReadJson\u003CDatos\u003E( Ruta( personaje ) );\n\t\tif ( datos == null ) return DeFabrica( personaje );\n\n\t\tNormalizar( datos );\n\t\treturn datos;\n\t}\n\n\tstatic Datos DeFabrica( string personaje )\n\t{\n\t\tvar datos = PartiturasDeFabrica.Obtener( personaje );\n\t\tif ( datos != null ) Normalizar( datos );\n\t\treturn datos;\n\t}\n\n\tpublic static void Guardar( string personaje, Datos datos )\n\t{\n\t\tif ( string.IsNullOrEmpty( personaje ) || datos == null ) return;\n\n\t\tFileSystem.Data.CreateDirectory( Carpeta );\n\t\tFileSystem.Data.WriteJson( Ruta( personaje ), datos );\n\t}\n\n\t/// \u003Csummary\u003E Vuelca los datos al componente vivo (copias, no referencias compartidas). \u003C/summary\u003E\n\tpublic static void AplicarA( PartituraCaras partitura, Datos datos )\n\t{\n\t\tif ( !partitura.IsValid() || datos == null ) return;\n\n\t\tpartitura.LoopA = new List\u003Cint\u003E( datos.LoopA );\n\t\tpartitura.LoopB = new List\u003Cint\u003E( datos.LoopB );\n\t\tpartitura.HorrorA = new List\u003Cint\u003E( datos.HorrorA );\n\t\tpartitura.HorrorB = new List\u003Cint\u003E( datos.HorrorB );\n\t}\n\n\t/// \u003Csummary\u003E Pistas siempre de 48 casilleros, venga como venga el JSON. \u003C/summary\u003E\n\tstatic void Normalizar( Datos datos )\n\t{\n\t\tdatos.LoopA = Ajustar( datos.LoopA );\n\t\tdatos.LoopB = Ajustar( datos.LoopB );\n\t\tdatos.HorrorA = Ajustar( datos.HorrorA );\n\t\tdatos.HorrorB = Ajustar( datos.HorrorB );\n\t}\n\n\tstatic List\u003Cint\u003E Ajustar( List\u003Cint\u003E pista )\n\t{\n\t\tpista ??= new List\u003Cint\u003E();\n\t\twhile ( pista.Count \u003C PartituraCaras.Casilleros ) pista.Add( -1 );\n\t\tif ( pista.Count \u003E PartituraCaras.Casilleros )\n\t\t\tpista.RemoveRange( PartituraCaras.Casilleros, pista.Count - PartituraCaras.Casilleros );\n\t\treturn pista;\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"TemblorCamara.cs","FileName":"TemblorCamara.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\n\n/// \u003Csummary\u003E\n/// [SBHBase] TEMBLOR DE C\u00C1MARA gen\u00E9rico (2026-08-10, pedido de H\u00E9ctor para\n/// los pisotones del Siren Head). Modelo de \u0022trauma\u0022: los golpes suman\n/// trauma, el trauma decae solo, y mientras haya trauma la vista vibra\n/// (offset \u002B roll aleatorios, curva cuadr\u00E1tica).\n///\n/// Es un ICameraModifier con el ORDEN M\u00C1S ALTO: corre al final de la cadena\n/// de c\u00E1mara del engine, sobre la vista YA calculada del frame \u2014 primera\n/// persona, posesi\u00F3n (\u00F3rbita), cine \u2014 as\u00ED que no hay realimentaci\u00F3n posible\n/// (la versi\u00F3n anterior mutaba la c\u00E1mara post-update y la \u00F3rbita de la\n/// posesi\u00F3n heredaba la sacudida: \u0022se buguea la c\u00E1mara\u0022, 2026-08-10).\n///\n/// Cualquier sistema puede llamar \u003Csee cref=\u0022Sacudir\u0022/\u003E: pisadas de gigante,\n/// explosiones, el tr\u00EDpode cuando vuelva del taller...\n/// \u003C/summary\u003E\npublic sealed class TemblorCamara : Component, ICameraModifier\n{\n\tstatic TemblorCamara _instancia;\n\tfloat _trauma;\n\n\t/// \u003Csummary\u003E Suma un golpe de temblor (0.1 = apenas, 1 = terremoto). \u003C/summary\u003E\n\tpublic static void Sacudir( Scene scene, float intensidad )\n\t{\n\t\tif ( intensidad \u003C= 0f ) return;\n\t\tvar t = Asegurar( scene );\n\t\tif ( t.IsValid() )\n\t\t\tt._trauma = ( t._trauma \u002B intensidad ).Clamp( 0f, 1.5f );\n\t}\n\n\t// Patr\u00F3n FadeHorror.Asegurar: un solo GameObject de servicio en la escena\n\tstatic TemblorCamara Asegurar( Scene scene )\n\t{\n\t\tif ( _instancia.IsValid() ) return _instancia;\n\t\tif ( scene == null ) return null;\n\n\t\tvar go = scene.CreateObject();\n\t\tgo.Name = \u0022sbh_temblor_camara\u0022;\n\t\tgo.Flags |= GameObjectFlags.NotSaved | GameObjectFlags.NotNetworked;\n\t\t_instancia = go.Components.Create\u003CTemblorCamara\u003E();\n\t\treturn _instancia;\n\t}\n\n\tint ICameraModifier.CameraOrder =\u003E 9999; // el \u00FAltimo de la cadena\n\n\tvoid ICameraModifier.ModifyCamera( CameraComponent camera, ref CameraView view )\n\t{\n\t\tif ( _trauma \u003C= 0.001f ) { _trauma = 0f; return; }\n\n\t\tvar fuerza = _trauma * _trauma;\n\t\tview.Position \u002B= Vector3.Random * ( 6f * fuerza );\n\t\tview.Rotation *= Rotation.From(\n\t\t\tGame.Random.Float( -1.2f, 1.2f ) * fuerza,\n\t\t\tGame.Random.Float( -1.2f, 1.2f ) * fuerza,\n\t\t\tGame.Random.Float( -2.5f, 2.5f ) * fuerza );\n\n\t\t// Decae solo: un golpe de 0.4 dura ~un tercio de segundo\n\t\t_trauma = System.MathF.Max( 0f, _trauma - Time.Delta * 2.2f );\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"Code/PercepcionNextbot.cs","FileName":"PercepcionNextbot.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// Filtro de percepci\u00F3n por objetivo, para que los packs modifiquen los\n/// sentidos sin tocar la base (el sigilo de Toy Zombies: te ven seg\u00FAn tu\n/// velocidad y tu linterna). Un componente del mismo GameObject lo\n/// implementa y PercepcionNextbot lo consulta en cada chequeo.\n/// \u003C/summary\u003E\npublic interface IFiltroPercepcion\n{\n\t/// \u003Csummary\u003E\n\t/// \u00BFPuede percibir a este objetivo a esta distancia? true = s\u00ED (aunque\n\t/// est\u00E9 fuera del cono: el ruido delata \u2014 la l\u00EDnea de visi\u00F3n igual se\n\t/// exige), false = no (silencioso/invisible), null = sin opini\u00F3n (siguen\n\t/// las reglas normales de la base).\n\t/// \u003C/summary\u003E\n\tbool? PuedeVer( IVida objetivo, float distancia );\n}\n\n/// \u003Csummary\u003E\n/// [SBHBase] Sentidos del nextbot: vista en cono con l\u00EDnea de visi\u00F3n real\n/// (las paredes bloquean) y o\u00EDdo de corto alcance en cualquier direcci\u00F3n.\n/// Recuerda al objetivo un rato despu\u00E9s de perderlo de vista, guardando su\n/// \u00FAltima posici\u00F3n conocida, y lo olvida pasado TiempoOlvido.\n/// Si un GameObject no tiene este componente, NextbotBrain vuelve al\n/// comportamiento omnisciente cl\u00E1sico.\n/// \u003C/summary\u003E\npublic sealed class PercepcionNextbot : Component\n{\n\t// ---------- Configurable desde el Inspector ----------\n\t[Property, Description( \u0022Distancia m\u00E1xima de visi\u00F3n\u0022 )]\n\tpublic float RangoVista { get; set; } = 1500f;\n\n\t[Property, Description( \u0022\u00C1ngulo total del cono de visi\u00F3n, en grados\u0022 )]\n\tpublic float AnguloVista { get; set; } = 140f;\n\n\t[Property, Description( \u0022Distancia a la que siente enemigos aunque est\u00E9n fuera del cono de visi\u00F3n (una pared igual lo bloquea)\u0022 )]\n\tpublic float RangoOido { get; set; } = 150f;\n\n\t[Property, Description( \u0022Altura de los ojos respecto al piso del nextbot\u0022 )]\n\tpublic float AlturaOjos { get; set; } = 60f;\n\n\t[Property, Description( \u0022Segundos que recuerda al objetivo tras perderlo de vista\u0022 )]\n\tpublic float TiempoOlvido { get; set; } = 5f;\n\n\t[Property, Description( \u0022Segundos entre escaneos de enemigos (rendimiento)\u0022 )]\n\tpublic float IntervaloEscaneo { get; set; } = 0.2f;\n\n\t[Property, Description( \u0022Multiplicador del rango de visi\u00F3n mientras est\u00E1 en alerta (tras recibir da\u00F1o)\u0022 )]\n\tpublic float MultiplicadorAlerta { get; set; } = 2f;\n\n\t[Property, Description( \u0022Segundos que dura la alerta tras recibir da\u00F1o (cada golpe la renueva)\u0022 )]\n\tpublic float DuracionAlerta { get; set; } = 15f;\n\n\t// ---------- Estado que consumen otros componentes ----------\n\t/// \u003Csummary\u003E\n\t/// Omnisciencia temporal: mientras est\u00E9 en true percibe a TODOS sus\n\t/// enemigos sin cono, sin paredes y sin l\u00EDmite de distancia (el\n\t/// SetOmniscient de DRGBase). La prenden skills de pack \u2014 el grito de\n\t/// manada del Mono \u2014 y la apagan ellas mismas al terminar.\n\t/// \u003C/summary\u003E\n\tpublic bool ModoOmnisciente { get; set; }\n\n\t/// \u003Csummary\u003E Enemigo actual (percibido ahora o recordado). Null si no hay. \u003C/summary\u003E\n\tpublic IVida ObjetivoActual { get; private set; }\n\n\t/// \u003Csummary\u003E \u00BFEst\u00E1 percibiendo al objetivo AHORA MISMO? \u003C/summary\u003E\n\tpublic bool ObjetivoVisible { get; private set; }\n\n\t/// \u003Csummary\u003E \u00DAltimo lugar donde percibi\u00F3 al objetivo. V\u00E1lido si ObjetivoActual no es null. \u003C/summary\u003E\n\tpublic Vector3 UltimaPosicionConocida { get; private set; }\n\n\t/// \u003Csummary\u003E Posici\u00F3n de los ojos, para traces de visi\u00F3n. \u003C/summary\u003E\n\tpublic Vector3 Ojos =\u003E WorldPosition \u002B Vector3.Up * AlturaOjos;\n\n\t/// \u003Csummary\u003E \u00BFEst\u00E1 en alerta? (lo da\u00F1aron hace poco: ve m\u00E1s lejos mientras busca) \u003C/summary\u003E\n\tpublic bool EnAlerta =\u003E _desdeAlerta \u003C DuracionAlerta;\n\n\t/// \u003Csummary\u003E Rango de visi\u00F3n vigente: el normal, o el ampliado si est\u00E1 en alerta. \u003C/summary\u003E\n\tpublic float RangoVistaEfectivo =\u003E EnAlerta ? RangoVista * MultiplicadorAlerta : RangoVista;\n\n\tTimeSince _desdeEscaneo;\n\tTimeSince _desdeVisto;\n\tTimeSince _desdeAlerta;\n\tVida _vida;\n\tNextbotBrain _cerebro;\n\n\t// La IA de este bot est\u00E1 apagada, sea por el switch global o por la\n\t// suspensi\u00F3n individual del cerebro (tecla apunt\u00E1ndolo). Sin sentidos ni\n\t// memoria \u2014 y de rebote SaltoNextbot tampoco salta (bebe de ObjetivoActual)\n\tbool IaApagada =\u003E !SbhConVars.IaActivada\n\t\t|| (_cerebro.IsValid() \u0026\u0026 _cerebro.IaSuspendida);\n\n\tprotected override void OnStart()\n\t{\n\t\t_desdeEscaneo = 999f;\n\t\t_desdeVisto = 999f;\n\t\t_desdeAlerta = 999f;\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_vida = Components.Get\u003CVida\u003E();\n\t\t_cerebro = Components.Get\u003CNextbotBrain\u003E();\n\t\tif ( _vida.IsValid() )\n\t\t\t_vida.AlRecibirDanio \u002B= AlSerDaniado;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( _vida.IsValid() )\n\t\t\t_vida.AlRecibirDanio -= AlSerDaniado;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Venganza: al recibir da\u00F1o, fija como objetivo a quien lo hizo aunque\n\t/// est\u00E9 fuera de los sentidos (un francotirador por la espalda, p. ej.).\n\t/// Sabe D\u00D3NDE estaba el atacante, no necesariamente lo ve.\n\t/// \u003C/summary\u003E\n\tvoid AlSerDaniado( float cantidad )\n\t{\n\t\tvar atacante = _vida?.UltimoAtacante;\n\t\tif ( !atacante.IsValid() || atacante == GameObject ) return;\n\n\t\t// El atacante puede ser el arma (hija del jugador): buscar hacia arriba\n\t\tvar objetivo = atacante.Components.Get\u003CIVida\u003E( FindMode.EverythingInSelfAndAncestors );\n\n\t\tif ( objetivo == null || objetivo.EstaMuerto ) return;\n\t\tif ( IaApagada ) return;\n\t\tif ( SbhConVars.IgnorarJugadores \u0026\u0026 objetivo.EsJugador ) return; // notarget total, ni con balazos\n\n\t\t// Alerta: mientras busca al agresor ve m\u00E1s lejos; se calma sola si no lo encuentra\n\t\t_desdeAlerta = 0f;\n\n\t\tObjetivoActual = objetivo;\n\t\tObjetivoVisible = false;\n\t\tUltimaPosicionConocida = objetivo.WorldPosition;\n\t\t_desdeVisto = 0f;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( _desdeEscaneo \u003C IntervaloEscaneo ) return;\n\t\t_desdeEscaneo = 0f;\n\n\t\tEscanear();\n\t}\n\n\tvoid Escanear()\n\t{\n\t\t// IA apagada (global o suspensi\u00F3n individual): sin sentidos ni memoria\n\t\tif ( IaApagada )\n\t\t{\n\t\t\tOlvidar();\n\t\t\treturn;\n\t\t}\n\n\t\t// Los muertos se olvidan al instante; los jugadores tambi\u00E9n si ahora los\n\t\t// ignoramos, y lo mismo quien sali\u00F3 del combate (posesi\u00F3n, cinem\u00E1ticas)\n\t\tif ( ObjetivoActual != null \u0026\u0026 (!ObjetivoActual.IsValid() || ObjetivoActual.EstaMuerto\n\t\t\t|| ObjetivoActual.FueraDeCombate\n\t\t\t|| (SbhConVars.IgnorarJugadores \u0026\u0026 ObjetivoActual.EsJugador)) )\n\t\t\tOlvidar();\n\n\t\t// Buscar el enemigo percibido m\u00E1s cercano\n\t\tvar miFaccion = Components.Get\u003CFaccion\u003E();\n\n\t\tIVida mejor = null;\n\t\tvar mejorDistancia = float.MaxValue;\n\n\t\tforeach ( var vida in Vidas.Todas( Scene ) )\n\t\t{\n\t\t\tif ( vida.GameObject == GameObject ) continue;\n\t\t\tif ( vida.EstaMuerto ) continue;\n\t\t\tif ( vida.FueraDeCombate ) continue;\n\t\t\tif ( SbhConVars.IgnorarJugadores \u0026\u0026 vida.EsJugador ) continue;\n\t\t\tif ( !Faccion.SonEnemigos( miFaccion, vida.GameObject.Components.Get\u003CFaccion\u003E() ) ) continue;\n\t\t\tif ( !Percibe( vida ) ) continue;\n\n\t\t\tvar distancia = vida.WorldPosition.Distance( WorldPosition );\n\t\t\tif ( distancia \u003C mejorDistancia )\n\t\t\t{\n\t\t\t\tmejorDistancia = distancia;\n\t\t\t\tmejor = vida;\n\t\t\t}\n\t\t}\n\n\t\tif ( mejor != null )\n\t\t{\n\t\t\tObjetivoActual = mejor;\n\t\t\tObjetivoVisible = true;\n\t\t\tUltimaPosicionConocida = mejor.WorldPosition;\n\t\t\t_desdeVisto = 0f;\n\t\t\treturn;\n\t\t}\n\n\t\t// No percibe a nadie: mantener el recuerdo un rato y despu\u00E9s olvidar\n\t\tObjetivoVisible = false;\n\n\t\tif ( ObjetivoActual != null \u0026\u0026 _desdeVisto \u003E TiempoOlvido )\n\t\t\tOlvidar();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Le marca un objetivo desde afuera, aunque no lo haya percibido: el\n\t/// equivalente al SpotEntity/SetEnemy de DRGBase. Lo usan las skills de\n\t/// manada (el grito del Mono se\u00F1ala a la v\u00EDctima para todo el pack).\n\t/// Entra como recuerdo \u2014 el escaneo siguiente decide si adem\u00E1s lo ve.\n\t/// \u003C/summary\u003E\n\tpublic void Marcar( IVida objetivo )\n\t{\n\t\tif ( objetivo == null || !objetivo.IsValid() || objetivo.EstaMuerto ) return;\n\t\tif ( IaApagada ) return;\n\t\tif ( SbhConVars.IgnorarJugadores \u0026\u0026 objetivo.EsJugador ) return;\n\n\t\tObjetivoActual = objetivo;\n\t\tObjetivoVisible = false;\n\t\tUltimaPosicionConocida = objetivo.WorldPosition;\n\t\t_desdeVisto = 0f;\n\t}\n\n\t/// \u003Csummary\u003E \u00BFEste enemigo entra por alguno de los sentidos? \u003C/summary\u003E\n\tpublic bool Percibe( IVida objetivo )\n\t{\n\t\t// Omnisciente: lo ve est\u00E9 donde est\u00E9, incluso atr\u00E1s y tras una pared\n\t\t// (pisa tambi\u00E9n al filtro: el grito de manada anula el sigilo).\n\t\t// Por skill (GritoManada) o por el interruptor GLOBAL de la base.\n\t\tif ( ModoOmnisciente || SbhConVars.Omnisciencia ) return true;\n\n\t\tvar distancia = objetivo.WorldPosition.Distance( WorldPosition );\n\n\t\t// Filtro del pack (sigilo): false = no lo percibe por m\u00E1s cerca que\n\t\t// est\u00E9; true = percibido aunque est\u00E9 fuera del cono (el ruido delata\n\t\t// la direcci\u00F3n), pero una pared igual lo esconde\n\t\tvar filtro = Components.Get\u003CIFiltroPercepcion\u003E();\n\t\tif ( filtro != null )\n\t\t{\n\t\t\tvar veredicto = filtro.PuedeVer( objetivo, distancia );\n\t\t\tif ( veredicto == false ) return false;\n\t\t\tif ( veredicto == true )\n\t\t\t\treturn distancia \u003C= RangoVistaEfectivo \u0026\u0026 TieneLineaDeVision( objetivo );\n\t\t}\n\n\t\t// O\u00EDdo: corto alcance sin importar el \u00E1ngulo, pero las paredes\n\t\t// S\u00CD bloquean (si no, es un radar y se siente omnisciente)\n\t\tif ( distancia \u003C= RangoOido )\n\t\t\treturn TieneLineaDeVision( objetivo );\n\n\t\t// Vista: alcance \u002B cono \u002B l\u00EDnea de visi\u00F3n libre\n\t\tif ( distancia \u003E RangoVistaEfectivo )\n\t\t\treturn false;\n\n\t\tvar direccion = (objetivo.WorldPosition - WorldPosition).WithZ( 0 );\n\t\tif ( direccion.Length \u003E 1f )\n\t\t{\n\t\t\tvar cosLimite = MathF.Cos( AnguloVista * 0.5f * MathF.PI / 180f );\n\t\t\tvar cosAngulo = Vector3.Dot( WorldRotation.Forward.WithZ( 0 ).Normal, direccion.Normal );\n\t\t\tif ( cosAngulo \u003C cosLimite )\n\t\t\t\treturn false;\n\t\t}\n\n\t\treturn TieneLineaDeVision( objetivo );\n\t}\n\n\tbool TieneLineaDeVision( IVida objetivo )\n\t{\n\t\t// Apuntar al torso del objetivo, no a sus pies\n\t\tvar puntoObjetivo = objetivo.WorldPosition \u002B Vector3.Up * 40f;\n\n\t\tvar trace = Scene.Trace.Ray( Ojos, puntoObjetivo )\n\t\t\t.IgnoreGameObjectHierarchy( GameObject )\n\t\t\t.IgnoreGameObjectHierarchy( objetivo.GameObject )\n\t\t\t.Run();\n\n\t\t// Si el rayo no choc\u00F3 con nada en el camino, lo ve\n\t\treturn !trace.Hit;\n\t}\n\n\tpublic void Olvidar()\n\t{\n\t\tObjetivoActual = null;\n\t\tObjetivoVisible = false;\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"PercepcionNextbot.cs","FileName":"PercepcionNextbot.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// Filtro de percepci\u00F3n por objetivo, para que los packs modifiquen los\n/// sentidos sin tocar la base (el sigilo de Toy Zombies: te ven seg\u00FAn tu\n/// velocidad y tu linterna). Un componente del mismo GameObject lo\n/// implementa y PercepcionNextbot lo consulta en cada chequeo.\n/// \u003C/summary\u003E\npublic interface IFiltroPercepcion\n{\n\t/// \u003Csummary\u003E\n\t/// \u00BFPuede percibir a este objetivo a esta distancia? true = s\u00ED (aunque\n\t/// est\u00E9 fuera del cono: el ruido delata \u2014 la l\u00EDnea de visi\u00F3n igual se\n\t/// exige), false = no (silencioso/invisible), null = sin opini\u00F3n (siguen\n\t/// las reglas normales de la base).\n\t/// \u003C/summary\u003E\n\tbool? PuedeVer( IVida objetivo, float distancia );\n}\n\n/// \u003Csummary\u003E\n/// [SBHBase] Sentidos del nextbot: vista en cono con l\u00EDnea de visi\u00F3n real\n/// (las paredes bloquean) y o\u00EDdo de corto alcance en cualquier direcci\u00F3n.\n/// Recuerda al objetivo un rato despu\u00E9s de perderlo de vista, guardando su\n/// \u00FAltima posici\u00F3n conocida, y lo olvida pasado TiempoOlvido.\n/// Si un GameObject no tiene este componente, NextbotBrain vuelve al\n/// comportamiento omnisciente cl\u00E1sico.\n/// \u003C/summary\u003E\npublic sealed class PercepcionNextbot : Component\n{\n\t// ---------- Configurable desde el Inspector ----------\n\t[Property, Description( \u0022Distancia m\u00E1xima de visi\u00F3n\u0022 )]\n\tpublic float RangoVista { get; set; } = 1500f;\n\n\t[Property, Description( \u0022\u00C1ngulo total del cono de visi\u00F3n, en grados\u0022 )]\n\tpublic float AnguloVista { get; set; } = 140f;\n\n\t[Property, Description( \u0022Distancia a la que siente enemigos aunque est\u00E9n fuera del cono de visi\u00F3n (una pared igual lo bloquea)\u0022 )]\n\tpublic float RangoOido { get; set; } = 150f;\n\n\t[Property, Description( \u0022Altura de los ojos respecto al piso del nextbot\u0022 )]\n\tpublic float AlturaOjos { get; set; } = 60f;\n\n\t[Property, Description( \u0022Segundos que recuerda al objetivo tras perderlo de vista\u0022 )]\n\tpublic float TiempoOlvido { get; set; } = 5f;\n\n\t[Property, Description( \u0022Segundos entre escaneos de enemigos (rendimiento)\u0022 )]\n\tpublic float IntervaloEscaneo { get; set; } = 0.2f;\n\n\t[Property, Description( \u0022Multiplicador del rango de visi\u00F3n mientras est\u00E1 en alerta (tras recibir da\u00F1o)\u0022 )]\n\tpublic float MultiplicadorAlerta { get; set; } = 2f;\n\n\t[Property, Description( \u0022Segundos que dura la alerta tras recibir da\u00F1o (cada golpe la renueva)\u0022 )]\n\tpublic float DuracionAlerta { get; set; } = 15f;\n\n\t// ---------- Estado que consumen otros componentes ----------\n\t/// \u003Csummary\u003E\n\t/// Omnisciencia temporal: mientras est\u00E9 en true percibe a TODOS sus\n\t/// enemigos sin cono, sin paredes y sin l\u00EDmite de distancia (el\n\t/// SetOmniscient de DRGBase). La prenden skills de pack \u2014 el grito de\n\t/// manada del Mono \u2014 y la apagan ellas mismas al terminar.\n\t/// \u003C/summary\u003E\n\tpublic bool ModoOmnisciente { get; set; }\n\n\t/// \u003Csummary\u003E Enemigo actual (percibido ahora o recordado). Null si no hay. \u003C/summary\u003E\n\tpublic IVida ObjetivoActual { get; private set; }\n\n\t/// \u003Csummary\u003E \u00BFEst\u00E1 percibiendo al objetivo AHORA MISMO? \u003C/summary\u003E\n\tpublic bool ObjetivoVisible { get; private set; }\n\n\t/// \u003Csummary\u003E \u00DAltimo lugar donde percibi\u00F3 al objetivo. V\u00E1lido si ObjetivoActual no es null. \u003C/summary\u003E\n\tpublic Vector3 UltimaPosicionConocida { get; private set; }\n\n\t/// \u003Csummary\u003E Posici\u00F3n de los ojos, para traces de visi\u00F3n. \u003C/summary\u003E\n\tpublic Vector3 Ojos =\u003E WorldPosition \u002B Vector3.Up * AlturaOjos;\n\n\t/// \u003Csummary\u003E \u00BFEst\u00E1 en alerta? (lo da\u00F1aron hace poco: ve m\u00E1s lejos mientras busca) \u003C/summary\u003E\n\tpublic bool EnAlerta =\u003E _desdeAlerta \u003C DuracionAlerta;\n\n\t/// \u003Csummary\u003E Rango de visi\u00F3n vigente: el normal, o el ampliado si est\u00E1 en alerta. \u003C/summary\u003E\n\tpublic float RangoVistaEfectivo =\u003E EnAlerta ? RangoVista * MultiplicadorAlerta : RangoVista;\n\n\tTimeSince _desdeEscaneo;\n\tTimeSince _desdeVisto;\n\tTimeSince _desdeAlerta;\n\tVida _vida;\n\tNextbotBrain _cerebro;\n\n\t// La IA de este bot est\u00E1 apagada, sea por el switch global o por la\n\t// suspensi\u00F3n individual del cerebro (tecla apunt\u00E1ndolo). Sin sentidos ni\n\t// memoria \u2014 y de rebote SaltoNextbot tampoco salta (bebe de ObjetivoActual)\n\tbool IaApagada =\u003E !SbhConVars.IaActivada\n\t\t|| (_cerebro.IsValid() \u0026\u0026 _cerebro.IaSuspendida);\n\n\tprotected override void OnStart()\n\t{\n\t\t_desdeEscaneo = 999f;\n\t\t_desdeVisto = 999f;\n\t\t_desdeAlerta = 999f;\n\t}\n\n\tprotected override void OnEnabled()\n\t{\n\t\t_vida = Components.Get\u003CVida\u003E();\n\t\t_cerebro = Components.Get\u003CNextbotBrain\u003E();\n\t\tif ( _vida.IsValid() )\n\t\t\t_vida.AlRecibirDanio \u002B= AlSerDaniado;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( _vida.IsValid() )\n\t\t\t_vida.AlRecibirDanio -= AlSerDaniado;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Venganza: al recibir da\u00F1o, fija como objetivo a quien lo hizo aunque\n\t/// est\u00E9 fuera de los sentidos (un francotirador por la espalda, p. ej.).\n\t/// Sabe D\u00D3NDE estaba el atacante, no necesariamente lo ve.\n\t/// \u003C/summary\u003E\n\tvoid AlSerDaniado( float cantidad )\n\t{\n\t\tvar atacante = _vida?.UltimoAtacante;\n\t\tif ( !atacante.IsValid() || atacante == GameObject ) return;\n\n\t\t// El atacante puede ser el arma (hija del jugador): buscar hacia arriba\n\t\tvar objetivo = atacante.Components.Get\u003CIVida\u003E( FindMode.EverythingInSelfAndAncestors );\n\n\t\tif ( objetivo == null || objetivo.EstaMuerto ) return;\n\t\tif ( IaApagada ) return;\n\t\tif ( SbhConVars.IgnorarJugadores \u0026\u0026 objetivo.EsJugador ) return; // notarget total, ni con balazos\n\n\t\t// Alerta: mientras busca al agresor ve m\u00E1s lejos; se calma sola si no lo encuentra\n\t\t_desdeAlerta = 0f;\n\n\t\tObjetivoActual = objetivo;\n\t\tObjetivoVisible = false;\n\t\tUltimaPosicionConocida = objetivo.WorldPosition;\n\t\t_desdeVisto = 0f;\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( _desdeEscaneo \u003C IntervaloEscaneo ) return;\n\t\t_desdeEscaneo = 0f;\n\n\t\tEscanear();\n\t}\n\n\tvoid Escanear()\n\t{\n\t\t// IA apagada (global o suspensi\u00F3n individual): sin sentidos ni memoria\n\t\tif ( IaApagada )\n\t\t{\n\t\t\tOlvidar();\n\t\t\treturn;\n\t\t}\n\n\t\t// Los muertos se olvidan al instante; los jugadores tambi\u00E9n si ahora los\n\t\t// ignoramos, y lo mismo quien sali\u00F3 del combate (posesi\u00F3n, cinem\u00E1ticas)\n\t\tif ( ObjetivoActual != null \u0026\u0026 (!ObjetivoActual.IsValid() || ObjetivoActual.EstaMuerto\n\t\t\t|| ObjetivoActual.FueraDeCombate\n\t\t\t|| (SbhConVars.IgnorarJugadores \u0026\u0026 ObjetivoActual.EsJugador)) )\n\t\t\tOlvidar();\n\n\t\t// Buscar el enemigo percibido m\u00E1s cercano\n\t\tvar miFaccion = Components.Get\u003CFaccion\u003E();\n\n\t\tIVida mejor = null;\n\t\tvar mejorDistancia = float.MaxValue;\n\n\t\tforeach ( var vida in Vidas.Todas( Scene ) )\n\t\t{\n\t\t\tif ( vida.GameObject == GameObject ) continue;\n\t\t\tif ( vida.EstaMuerto ) continue;\n\t\t\tif ( vida.FueraDeCombate ) continue;\n\t\t\tif ( SbhConVars.IgnorarJugadores \u0026\u0026 vida.EsJugador ) continue;\n\t\t\tif ( !Faccion.SonEnemigos( miFaccion, vida.GameObject.Components.Get\u003CFaccion\u003E() ) ) continue;\n\t\t\tif ( !Percibe( vida ) ) continue;\n\n\t\t\tvar distancia = vida.WorldPosition.Distance( WorldPosition );\n\t\t\tif ( distancia \u003C mejorDistancia )\n\t\t\t{\n\t\t\t\tmejorDistancia = distancia;\n\t\t\t\tmejor = vida;\n\t\t\t}\n\t\t}\n\n\t\tif ( mejor != null )\n\t\t{\n\t\t\tObjetivoActual = mejor;\n\t\t\tObjetivoVisible = true;\n\t\t\tUltimaPosicionConocida = mejor.WorldPosition;\n\t\t\t_desdeVisto = 0f;\n\t\t\treturn;\n\t\t}\n\n\t\t// No percibe a nadie: mantener el recuerdo un rato y despu\u00E9s olvidar\n\t\tObjetivoVisible = false;\n\n\t\tif ( ObjetivoActual != null \u0026\u0026 _desdeVisto \u003E TiempoOlvido )\n\t\t\tOlvidar();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Le marca un objetivo desde afuera, aunque no lo haya percibido: el\n\t/// equivalente al SpotEntity/SetEnemy de DRGBase. Lo usan las skills de\n\t/// manada (el grito del Mono se\u00F1ala a la v\u00EDctima para todo el pack).\n\t/// Entra como recuerdo \u2014 el escaneo siguiente decide si adem\u00E1s lo ve.\n\t/// \u003C/summary\u003E\n\tpublic void Marcar( IVida objetivo )\n\t{\n\t\tif ( objetivo == null || !objetivo.IsValid() || objetivo.EstaMuerto ) return;\n\t\tif ( IaApagada ) return;\n\t\tif ( SbhConVars.IgnorarJugadores \u0026\u0026 objetivo.EsJugador ) return;\n\n\t\tObjetivoActual = objetivo;\n\t\tObjetivoVisible = false;\n\t\tUltimaPosicionConocida = objetivo.WorldPosition;\n\t\t_desdeVisto = 0f;\n\t}\n\n\t/// \u003Csummary\u003E \u00BFEste enemigo entra por alguno de los sentidos? \u003C/summary\u003E\n\tpublic bool Percibe( IVida objetivo )\n\t{\n\t\t// Omnisciente: lo ve est\u00E9 donde est\u00E9, incluso atr\u00E1s y tras una pared\n\t\t// (pisa tambi\u00E9n al filtro: el grito de manada anula el sigilo).\n\t\t// Por skill (GritoManada) o por el interruptor GLOBAL de la base.\n\t\tif ( ModoOmnisciente || SbhConVars.Omnisciencia ) return true;\n\n\t\tvar distancia = objetivo.WorldPosition.Distance( WorldPosition );\n\n\t\t// Filtro del pack (sigilo): false = no lo percibe por m\u00E1s cerca que\n\t\t// est\u00E9; true = percibido aunque est\u00E9 fuera del cono (el ruido delata\n\t\t// la direcci\u00F3n), pero una pared igual lo esconde\n\t\tvar filtro = Components.Get\u003CIFiltroPercepcion\u003E();\n\t\tif ( filtro != null )\n\t\t{\n\t\t\tvar veredicto = filtro.PuedeVer( objetivo, distancia );\n\t\t\tif ( veredicto == false ) return false;\n\t\t\tif ( veredicto == true )\n\t\t\t\treturn distancia \u003C= RangoVistaEfectivo \u0026\u0026 TieneLineaDeVision( objetivo );\n\t\t}\n\n\t\t// O\u00EDdo: corto alcance sin importar el \u00E1ngulo, pero las paredes\n\t\t// S\u00CD bloquean (si no, es un radar y se siente omnisciente)\n\t\tif ( distancia \u003C= RangoOido )\n\t\t\treturn TieneLineaDeVision( objetivo );\n\n\t\t// Vista: alcance \u002B cono \u002B l\u00EDnea de visi\u00F3n libre\n\t\tif ( distancia \u003E RangoVistaEfectivo )\n\t\t\treturn false;\n\n\t\tvar direccion = (objetivo.WorldPosition - WorldPosition).WithZ( 0 );\n\t\tif ( direccion.Length \u003E 1f )\n\t\t{\n\t\t\tvar cosLimite = MathF.Cos( AnguloVista * 0.5f * MathF.PI / 180f );\n\t\t\tvar cosAngulo = Vector3.Dot( WorldRotation.Forward.WithZ( 0 ).Normal, direccion.Normal );\n\t\t\tif ( cosAngulo \u003C cosLimite )\n\t\t\t\treturn false;\n\t\t}\n\n\t\treturn TieneLineaDeVision( objetivo );\n\t}\n\n\tbool TieneLineaDeVision( IVida objetivo )\n\t{\n\t\t// Apuntar al torso del objetivo, no a sus pies\n\t\tvar puntoObjetivo = objetivo.WorldPosition \u002B Vector3.Up * 40f;\n\n\t\tvar trace = Scene.Trace.Ray( Ojos, puntoObjetivo )\n\t\t\t.IgnoreGameObjectHierarchy( GameObject )\n\t\t\t.IgnoreGameObjectHierarchy( objetivo.GameObject )\n\t\t\t.Run();\n\n\t\t// Si el rayo no choc\u00F3 con nada en el camino, lo ve\n\t\treturn !trace.Hit;\n\t}\n\n\tpublic void Olvidar()\n\t{\n\t\tObjetivoActual = null;\n\t\tObjetivoVisible = false;\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"RecetaFusion.cs","FileName":"RecetaFusion.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Receta de fusi\u00F3n: N ingredientes (por nombre visible en escena,\n/// con cantidad) \u2192 una entidad resultado. Los ingredientes pueden ser\n/// nextbots (su Print Name: \u0022Raddy\u0022) o props (el nombre con el que spawnean\n/// del men\u00FA Q) \u2014 la tool Merge no distingue. Las recetas son DATOS del pack:\n/// cada pack trae sus archivos .fusion en Assets y la tool las levanta sola\n/// de ResourceLibrary. El mecanismo vive en la base; los personajes, no.\n/// \u003C/summary\u003E\n[AssetType( Name = \u0022SBHBase Fusion Recipe\u0022, Extension = \u0022fusion\u0022, Category = \u0022SBHBase\u0022 )]\npublic class RecetaFusion : GameResource\n{\n\t[Property, Description( \u0022Qu\u00E9 hace falta juntar: nombre visible y cantidad de cada ingrediente\u0022 )]\n\tpublic List\u003CIngredienteFusion\u003E Ingredientes { get; set; } = new();\n\n\t[Property, Description( \u0022Prefab que aparece al completar la receta\u0022 )]\n\tpublic PrefabFile Resultado { get; set; }\n\n\t[Property, Description( \u0022Sonido al fusionar (opcional)\u0022 )]\n\tpublic SoundEvent Sonido { get; set; }\n}\n\n/// \u003Csummary\u003E Un ingrediente de la receta: nombre visible \u002B cu\u00E1ntos. \u003C/summary\u003E\npublic class IngredienteFusion\n{\n\t[Property, Description( \u0022Identidad del ingrediente: Print Name del bot, o nombre del modelo del prop (\\\u0022watermelon\\\u0022); no distingue may\u00FAsculas\u0022 )]\n\tpublic string Nombre { get; set; } = \u0022\u0022;\n\n\t[Property, Description( \u0022Cu\u00E1ntos hacen falta\u0022 )]\n\tpublic int Cantidad { get; set; } = 1;\n\n\t[Property, Description( \u0022Ruta de asset para la miniatura del recetario (vac\u00EDo = se busca el .sent por nombre; para props, la ruta del .vmdl)\u0022 )]\n\tpublic string Icono { get; set; }\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"SaltoNextbot.cs","FileName":"SaltoNextbot.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// [SBHBase] Salto de nextbot: cuando persigue y el objetivo est\u00E1 a otro\n/// nivel \u2014 arriba de un contenedor, abajo de un puente \u2014 o el camino dej\u00F3\n/// de avanzar, se lanza hacia \u00E9l con una par\u00E1bola bal\u00EDstica. Sin miedo a\n/// caer: si el objetivo est\u00E1 abajo, salta desde el borde directamente.\n/// Durante el vuelo toma el control del movimiento (apaga NavMeshAgent y\n/// NextbotBrain) y lo devuelve al aterrizar, reencajando el agente en el\n/// navmesh. Usa el objetivo de PercepcionNextbot; sin ella no salta.\n/// \u003C/summary\u003E\npublic sealed class SaltoNextbot : Component\n{\n\t[RequireComponent] NavMeshAgent Agent { get; set; }\n\t[RequireComponent] NextbotBrain Cerebro { get; set; }\n\n\t[Property, Description( \u0022Desnivel m\u00EDnimo con el objetivo para plantearse saltar\u0022 )]\n\tpublic float DesnivelMinimo { get; set; } = 80f;\n\n\t[Property, Description( \u0022Distancia horizontal m\u00E1xima que cubre un salto\u0022 )]\n\tpublic float DistanciaMaxima { get; set; } = 500f;\n\n\t[Property, Description( \u0022Altura extra del arco por encima del punto m\u00E1s alto del salto\u0022 )]\n\tpublic float AlturaArco { get; set; } = 60f;\n\n\t[Property, Description( \u0022Gravedad durante el vuelo\u0022 )]\n\tpublic float Gravedad { get; set; } = 800f;\n\n\t[Property, Description( \u0022Segundos m\u00EDnimos entre saltos\u0022 )]\n\tpublic float Cooldown { get; set; } = 2f;\n\n\t[Property, Description( \u0022Si persiguiendo no avanza durante este tiempo (seg), salta hacia el objetivo aunque no haya desnivel\u0022 )]\n\tpublic float TiempoAtascado { get; set; } = 1.5f;\n\n\t[Property, Description( \u0022Distancia de contacto para conectar el cabezazo en pleno vuelo (0 = sin golpe a\u00E9reo)\u0022 )]\n\tpublic float RangoGolpeAereo { get; set; } = 60f;\n\n\t[Property, Description( \u0022Par\u00E1metro bool del animgraph para la anim de ca\u00EDda durante el vuelo (vac\u00EDo = sin anim)\u0022 )]\n\tpublic string ParametroCaida { get; set; } = \u0022b_fall\u0022;\n\n\t[Property, Description( \u0022Velocidad vertical del salto CUANDO LO POSEE UN JUGADOR (la IA usa el arco bal\u00EDstico de arriba). 330 = salto de persona; un coloso necesita mucho m\u00E1s\u0022 )]\n\tpublic float FuerzaSaltoPosesion { get; set; } = 330f;\n\n\t// --- Multiplicador global en vivo (sbh.salto.mult) ---\n\t// Escala arco y alcance sin tocar los prefabs: se tunea jugando y despu\u00E9s\n\t// se hornean los valores por personaje.\n\tfloat Mult =\u003E System.MathF.Max( SbhConVars.SaltoMultiplicador, 0.05f );\n\tfloat DistanciaEfectiva =\u003E DistanciaMaxima * Mult;\n\tfloat ArcoEfectivo =\u003E AlturaArco * Mult;\n\n\t/// \u003Csummary\u003E La fuerza de salto para la posesi\u00F3n, ya con el multiplicador global. \u003C/summary\u003E\n\tpublic float FuerzaSaltoPosesionEfectiva =\u003E FuerzaSaltoPosesion * Mult;\n\n\t/// \u003Csummary\u003E \u00BFEst\u00E1 en el aire ahora mismo? (para que otros componentes lo consulten) \u003C/summary\u003E\n\tpublic bool Volando { get; private set; }\n\n\tPercepcionNextbot _percepcion;\n\tVida _vida;\n\tVector3 _velocidad;\n\tTimeSince _desdeSalto;\n\tTimeSince _desdeDespegue;\n\tVector3 _posicionAnterior;\n\tTimeSince _desdeAvance;\n\n\tprotected override void OnStart()\n\t{\n\t\t_percepcion = Components.Get\u003CPercepcionNextbot\u003E();\n\t\t_vida = Components.Get\u003CVida\u003E();\n\t\t_desdeSalto = 999f;\n\t\t_posicionAnterior = WorldPosition;\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\tif ( Volando )\n\t\t\tAterrizar( WorldPosition );\n\t}\n\n\tprotected override void OnFixedUpdate()\n\t{\n\t\tif ( Volando )\n\t\t{\n\t\t\tVolar();\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _vida.IsValid() \u0026\u0026 _vida.EstaMuerto ) return;\n\t\tif ( _desdeSalto \u003C Cooldown ) return;\n\n\t\tvar destino = BuscarDestino();\n\t\tif ( destino == null )\n\t\t{\n\t\t\t_posicionAnterior = WorldPosition;\n\t\t\t_desdeAvance = 0f;\n\t\t\treturn;\n\t\t}\n\n\t\t// Detecci\u00F3n de atasco: \u00BFavanzamos algo desde la \u00FAltima vez?\n\t\tif ( WorldPosition.Distance( _posicionAnterior ) \u003E 30f )\n\t\t{\n\t\t\t_posicionAnterior = WorldPosition;\n\t\t\t_desdeAvance = 0f;\n\t\t}\n\n\t\tvar desnivel = destino.Value.z - WorldPosition.z;\n\t\tvar distanciaH = (destino.Value - WorldPosition).WithZ( 0 ).Length;\n\n\t\tif ( distanciaH \u003E DistanciaEfectiva ) return;\n\n\t\tvar hayDesnivel = MathF.Abs( desnivel ) \u003E DesnivelMinimo;\n\t\tvar atascado = _desdeAvance \u003E TiempoAtascado;\n\n\t\tif ( !hayDesnivel \u0026\u0026 !atascado ) return;\n\t\tif ( !hayDesnivel \u0026\u0026 distanciaH \u003C 60f ) return; // ya est\u00E1 encima, no hace falta\n\n\t\t// \u00BFEl arco CABE? Con el objetivo arriba de un techo/losa, el bot de\n\t\t// abajo saltaba una y otra vez contra el cielorraso queriendo\n\t\t// traspasarlo. Si algo del mundo tapa la vertical hasta el \u00E1pice del\n\t\t// salto, no hay salto \u2014 se sigue por navmesh como cualquier hijo de\n\t\t// vecino (y el pr\u00F3ximo intento ser\u00E1 desde otro lugar).\n\t\tvar apice = MathF.Max( WorldPosition.z, destino.Value.z ) \u002B ArcoEfectivo;\n\t\tvar alturaVuelo = apice - WorldPosition.z;\n\t\tif ( alturaVuelo \u003E 24f )\n\t\t{\n\t\t\tvar techo = Scene.Trace\n\t\t\t\t.Ray( WorldPosition \u002B Vector3.Up * 24f, WorldPosition \u002B Vector3.Up * alturaVuelo )\n\t\t\t\t.Radius( 14f )\n\t\t\t\t.IgnoreGameObjectHierarchy( GameObject )\n\t\t\t\t.WithoutTags( \u0022sbh_nextbot\u0022, \u0022player\u0022 )\n\t\t\t\t.Run();\n\n\t\t\tif ( techo.Hit ) return; // hay techo: el salto no cabe\n\t\t}\n\n\t\tSaltar( destino.Value );\n\t}\n\n\t/// \u003Csummary\u003E Hacia d\u00F3nde saltar, o null si no hay persecuci\u00F3n activa. \u003C/summary\u003E\n\tVector3? BuscarDestino()\n\t{\n\t\tif ( !_percepcion.IsValid() || !_percepcion.Enabled ) return null;\n\n\t\tvar objetivo = _percepcion.ObjetivoActual;\n\t\tif ( objetivo == null || !objetivo.IsValid() || objetivo.EstaMuerto ) return null;\n\n\t\treturn _percepcion.ObjetivoVisible\n\t\t\t? objetivo.WorldPosition\n\t\t\t: _percepcion.UltimaPosicionConocida;\n\t}\n\n\tvoid Saltar( Vector3 destino )\n\t{\n\t\tvar origen = WorldPosition;\n\n\t\t// Bal\u00EDstica: subir hasta AlturaArco por encima del punto m\u00E1s alto y\n\t\t// caer justo en el destino. vz por energ\u00EDa, tiempo por cinem\u00E1tica.\n\t\tvar apex = MathF.Max( 0f, destino.z - origen.z ) \u002B ArcoEfectivo;\n\t\tvar vz = MathF.Sqrt( 2f * Gravedad * apex );\n\n\t\tvar raiz = vz * vz - 2f * Gravedad * (destino.z - origen.z);\n\t\tif ( raiz \u003C 0f ) return;\n\n\t\tvar tiempo = (vz \u002B MathF.Sqrt( raiz )) / Gravedad;\n\t\tif ( tiempo \u003C 0.1f ) return;\n\n\t\tvar horizontal = (destino - origen).WithZ( 0 );\n\t\t_velocidad = horizontal / tiempo \u002B Vector3.Up * vz;\n\n\t\tif ( horizontal.Length \u003E 1f )\n\t\t\tWorldRotation = Rotation.LookAt( horizontal.Normal, Vector3.Up );\n\n\t\tAgent.Enabled = false;\n\t\tCerebro.Enabled = false;\n\n\t\t// En el aire: anim de ca\u00EDda (la misma que usa la physgun)\n\t\tif ( !string.IsNullOrEmpty( ParametroCaida ) )\n\t\t\tComponents.Get\u003CSkinnedModelRenderer\u003E()?.Set( ParametroCaida, true );\n\n\t\tVolando = true;\n\t\t_desdeDespegue = 0f;\n\t\t_desdeSalto = 0f;\n\t}\n\n\tvoid Volar()\n\t{\n\t\tvar dt = Time.Delta;\n\t\t_velocidad \u002B= Vector3.Down * Gravedad * dt;\n\n\t\t// Avanzar con una esfera a la altura del torso para chocar con el mundo\n\t\tvar desde = WorldPosition \u002B Vector3.Up * 24f;\n\t\tvar hasta = desde \u002B _velocidad * dt;\n\n\t\tvar trace = Scene.Trace.Ray( desde, hasta )\n\t\t\t.Radius( 14f )\n\t\t\t.IgnoreGameObjectHierarchy( GameObject )\n\t\t\t.WithoutTags( \u0022sbh_nextbot\u0022 ) // otro nextbot no es piso: atravesarlo, no aterrizar encima\n\t\t\t.Run();\n\n\t\tif ( trace.Hit )\n\t\t{\n\t\t\t// Piso (o algo pisable) mientras cae: aterrizar\n\t\t\tif ( _velocidad.z \u003C= 0f || trace.Normal.z \u003E 0.5f )\n\t\t\t{\n\t\t\t\tAterrizar( trace.EndPosition - Vector3.Up * 24f );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Pared en plena subida: cortar el avance y dejar que caiga\n\t\t\t_velocidad = _velocidad.WithX( 0f ).WithY( 0f );\n\t\t\tWorldPosition = trace.EndPosition - Vector3.Up * 24f;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tWorldPosition = hasta - Vector3.Up * 24f;\n\t\t}\n\n\t\t// Cabezazo en vuelo: si el arco pasa por encima del objetivo, conecta\n\t\tIntentarGolpeAereo();\n\n\t\t// Seguridad: nunca volar para siempre (agujeros, ca\u00EDdas al vac\u00EDo)\n\t\tif ( _desdeDespegue \u003E 4f )\n\t\t\tAterrizar( WorldPosition );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// El pounce: en pleno vuelo, si el objetivo queda a distancia de cuerpo,\n\t/// el golpe conecta ah\u00ED mismo \u2014 sin retardo, el contacto ES el impacto.\n\t/// El cooldown de ataque del cerebro evita golpear dos veces por salto.\n\t/// \u003C/summary\u003E\n\tvoid IntentarGolpeAereo()\n\t{\n\t\tif ( RangoGolpeAereo \u003C= 0f || !Cerebro.IsValid() ) return;\n\n\t\tvar objetivo = _percepcion.IsValid() ? _percepcion.ObjetivoActual : null;\n\t\tif ( objetivo == null || !objetivo.IsValid() || objetivo.EstaMuerto ) return;\n\n\t\t// Medir desde el torso en vuelo contra el torso del objetivo\n\t\tvar torso = WorldPosition \u002B Vector3.Up * 24f;\n\t\tvar torsoObjetivo = objetivo.WorldPosition \u002B Vector3.Up * 32f;\n\n\t\tif ( torso.Distance( torsoObjetivo ) \u003E RangoGolpeAereo ) return;\n\n\t\tCerebro.GolpeDirecto( objetivo );\n\t}\n\n\tvoid Aterrizar( Vector3 posicion )\n\t{\n\t\tVolando = false;\n\t\tWorldPosition = posicion;\n\n\t\tif ( !string.IsNullOrEmpty( ParametroCaida ) )\n\t\t\tComponents.Get\u003CSkinnedModelRenderer\u003E()?.Set( ParametroCaida, false );\n\n\t\t// Si muri\u00F3 en el aire, MuerteNextbot ya apag\u00F3 la IA: no revivirla\n\t\tif ( _vida.IsValid() \u0026\u0026 _vida.EstaMuerto ) return;\n\n\t\t// Pisada de gigante: el aterrizaje retumba (m\u00E1s fuerte que el paso)\n\t\tComponents.Get\u003CPisadaGigante\u003E()?.AlAterrizar();\n\n\t\tif ( Agent.IsValid() )\n\t\t{\n\t\t\tAgent.Enabled = true;\n\t\t\tAgent.SetAgentPosition( posicion );\n\t\t}\n\n\t\tif ( Cerebro.IsValid() )\n\t\t\tCerebro.Enabled = true;\n\t}\n}\n"},{"Ident":"ksbravo.sbhbase","Path":"CabezaNextbot.cs","FileName":"CabezaNextbot.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":342288,"Code":"namespace SBH;\n\nusing Sandbox;\nusing System;\n\n/// \u003Csummary\u003E\n/// [SBHBase] La cabeza del nextbot sigue al objetivo, estilo DRGBase.\n/// Alimenta el nodo Look At del animgraph compartido (sbh_nextbot.vanmgrph)\n/// v\u00EDa \u003Csee cref=\u0022SkinnedModelRenderer.SetLookDirection\u0022/\u003E: escribe el\n/// par\u00E1metro vector (direcci\u00F3n de mirada) y su peso, y el engine gira la\n/// cadena de huesos LookChain del vmdl ENCIMA de la animaci\u00F3n viva \u2014 la\n/// cabeza conserva el rebote de correr mientras mira al objetivo, sin\n/// bone-overrides ni anclas (el enfoque anterior peleaba con el snapshot\n/// de huesos del engine y qued\u00F3 descartado).\n/// Es el equivalente del autoplay blend del QC de GMod.\n/// Usa el objetivo de PercepcionNextbot; sin ese componente no hace nada.\n/// Requiere que el modelo tenga animgraph con el par\u00E1metro y una LookChain;\n/// si no los tiene, SetLookDirection simplemente no hace nada (inofensivo).\n/// \u003C/summary\u003E\npublic sealed class CabezaNextbot : Component\n{\n\t[RequireComponent] SkinnedModelRenderer Renderer { get; set; }\n\n\t[Property, Description( \u0022Giro horizontal m\u00E1ximo hacia cada lado, en grados\u0022 )]\n\tpublic float LimiteYaw { get; set; } = 70f;\n\n\t[Property, Description( \u0022Inclinaci\u00F3n vertical m\u00E1xima (arriba/abajo), en grados\u0022 )]\n\tpublic float LimitePitch { get; set; } = 35f;\n\n\t[Property, Description( \u0022Velocidad de seguimiento (m\u00E1s alto = m\u00E1s brusco)\u0022 )]\n\tpublic float Velocidad { get; set; } = 8f;\n\n\t[Property, Description( \u0022Altura sobre el origen del objetivo a la que mira (sus ojos)\u0022 )]\n\tpublic float AlturaMirada { get; set; } = 55f;\n\n\t[Property, Description( \u0022Nombre del par\u00E1metro vector de mirada en el animgraph (el peso va en \u003Cnombre\u003E_weight)\u0022 )]\n\tpublic string ParametroMirada { get; set; } = \u0022aim_head\u0022;\n\n\t[Property, Description( \u0022Calibraci\u00F3n fina: grados que se suman al yaw calculado (positivo = m\u00E1s a la izquierda). Para corregir desv\u00EDos si el frame del modelo no qued\u00F3 perfecto\u0022 )]\n\tpublic float DesvioYaw { get; set; } = 0f;\n\n\t[Property, Description( \u0022Calibraci\u00F3n fina: grados que se suman al pitch calculado (positivo = m\u00E1s abajo)\u0022 )]\n\tpublic float DesvioPitch { get; set; } = 0f;\n\n\tPercepcionNextbot _percepcion;\n\tVida _vida;\n\tVector3 _direccion;   // direcci\u00F3n de mirada suavizada, en espacio de mundo\n\tfloat _peso;          // 0 = la animaci\u00F3n manda, 1 = mirada plena\n\tstring _personaje;    // para el interruptor SinCabeza de EventosAnim\n\n\tprotected override void OnStart()\n\t{\n\t\t_percepcion = Components.Get\u003CPercepcionNextbot\u003E();\n\t\t_vida = Components.Get\u003CVida\u003E();\n\t\t_direccion = WorldRotation.Forward;\n\t\t_personaje = EventosAnim.PersonajeDe( Renderer );\n\t}\n\n\tprotected override void OnDisabled()\n\t{\n\t\t// Devolverle la cabeza a la animaci\u00F3n (muerte \u2192 ragdoll, IA apagada...)\n\t\t_peso = 0f;\n\t\t_direccion = WorldRotation.Forward;\n\n\t\tif ( Renderer.IsValid() )\n\t\t\tRenderer.SetLookDirection( ParametroMirada, _direccion, 0f );\n\t}\n\n\tprotected override void OnUpdate()\n\t{\n\t\tif ( !Renderer.IsValid() ) return;\n\n\t\tvar puntoMirada = BuscarPuntoMirada();\n\n\t\tvar dirDeseada = WorldRotation.Forward;\n\t\tvar pesoDeseado = 0f;\n\n\t\tif ( puntoMirada != null )\n\t\t{\n\t\t\tvar origen = WorldPosition \u002B Vector3.Up * AlturaMirada;\n\t\t\tvar (yaw, pitch) = CalcularAngulos( puntoMirada.Value, origen );\n\n\t\t\t// Demasiado a la espalda: soltar la mirada en vez de retorcer el cuello\n\t\t\tif ( yaw != null )\n\t\t\t{\n\t\t\t\t// Reconstruir la direcci\u00F3n ya clampeada a los l\u00EDmites del cuello,\n\t\t\t\t// con la calibraci\u00F3n fina encima (corrige frames imperfectos)\n\t\t\t\tdirDeseada = WorldRotation * Rotation.From( pitch.Value \u002B DesvioPitch, yaw.Value \u002B DesvioYaw, 0f ) * Vector3.Forward;\n\t\t\t\tpesoDeseado = 1f;\n\t\t\t}\n\t\t}\n\n\t\t// Suavizado independiente del framerate; el nodo del animgraph trae su\n\t\t// propio amortiguador (spring), esto solo gobierna entrar/salir de mirada\n\t\tvar t = 1f - MathF.Exp( -Velocidad * Time.Delta );\n\t\t_direccion = Vector3.Slerp( _direccion, dirDeseada, t ).Normal;\n\t\t_peso = _peso.LerpTo( pesoDeseado, t );\n\n\t\tRenderer.SetLookDirection( ParametroMirada, _direccion, _peso );\n\t}\n\n\t/// \u003Csummary\u003E D\u00F3nde mirar, o null si no hay nada que mirar (o estamos muertos). \u003C/summary\u003E\n\tVector3? BuscarPuntoMirada()\n\t{\n\t\t// Interruptor por personaje (2026-08-10): rigs GMod donde \u0022head\u0022 es\n\t\t// una parte especial (tapa del libro de Buuik, tapa del pastel de\n\t\t// Cake) \u2014 el peso baja a 0 con el suavizado de siempre\n\t\tif ( EventosAnim.CabezaApagada( _personaje ) ) return null;\n\n\t\tif ( _vida.IsValid() \u0026\u0026 _vida.EstaMuerto ) return null;\n\t\tif ( !_percepcion.IsValid() || !_percepcion.Enabled ) return null;\n\n\t\tvar objetivo = _percepcion.ObjetivoActual;\n\t\tif ( objetivo == null || !objetivo.IsValid() || objetivo.EstaMuerto ) return null;\n\n\t\tvar punto = _percepcion.ObjetivoVisible\n\t\t\t? objetivo.WorldPosition\n\t\t\t: _percepcion.UltimaPosicionConocida;\n\n\t\treturn punto \u002B Vector3.Up * AlturaMirada;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// \u00C1ngulos hacia el punto en espacio del cuerpo, ya clampeados a los\n\t/// l\u00EDmites, o (null, null) si el objetivo queda demasiado a la espalda.\n\t/// \u003C/summary\u003E\n\t(float? yaw, float? pitch) CalcularAngulos( Vector3 puntoMirada, Vector3 origen )\n\t{\n\t\tvar dirLocal = WorldRotation.Inverse * (puntoMirada - origen).Normal;\n\n\t\tvar yaw = MathF.Atan2( dirLocal.y, dirLocal.x ).RadianToDegree();\n\t\tvar pitch = -MathF.Asin( dirLocal.z.Clamp( -1f, 1f ) ).RadianToDegree();\n\n\t\tif ( MathF.Abs( yaw ) \u003E 120f ) return (null, null);\n\n\t\treturn (yaw.Clamp( -LimiteYaw, LimiteYaw ), pitch.Clamp( -LimitePitch, LimitePitch ));\n\t}\n}\n"}]}