Component attached to enemy prefabs that handles death behavior for nextbots. When Vida signals death it stops AI and sensory components, optionally plays a death sound, turns the model into a ragdoll (or creates a Rigidbody fallback), applies impulses and damping, waits a configurable time, then fades out and destroys the GameObject.
namespace SBH;
using Sandbox;
using System;
/// <summary>
/// Muerte del nextbot: al llegar a 0 de Vida, apaga IA y sonido,
/// convierte el cuerpo en ragdoll (física del vmdl) y pasado un tiempo
/// lo desvanece en vez de borrarlo de golpe. Si el modelo no tiene
/// física compilada, usa Rigidbody + la cápsula como ragdoll rígido.
/// Va en el prefab del sprunki, junto a Vida.
/// </summary>
public sealed class MuerteNextbot : Component
{
[RequireComponent] Vida Vida { get; set; }
[Property, Description( "Sonido al morir (opcional)" )]
public SoundEvent SonidoMuerte { get; set; }
[Property, Description( "Segundos hasta que el cuerpo empieza a desvanecerse (0 = se queda para siempre)" )]
public float TiempoDespawn { get; set; } = 10f;
[Property, Description( "Segundos que dura el desvanecimiento" )]
public float TiempoDesvanecer { get; set; } = 2f;
[Property, Description( "Fuerza del empujón al morir (para que se desplome)" )]
public float FuerzaEmpujon { get; set; } = 5f;
[Property, Description( "Secuencia del vmdl para la pose del cadáver (evita que quede congelado corriendo)" )]
public string AnimReposo { get; set; } = "idle";
[Property, Description( "Peso aparente: los impulsos de muerte (empujón, ejecución) se DIVIDEN por esto. 1 = normal; los colosos usan su altura/150 para no volar como papel" )]
public float Peso { get; set; } = 1f;
[Property, Description( "Amortiguación del ragdoll articulado (colosos): frena el cadáver que se va de viaje o gira solo cuando el solver pelea entre cuerpos grandes. 0 = física pura, el default de siempre. Probar 1-3 en gigantes" )]
public float AmortiguacionRagdoll { get; set; } = 0f;
[Property, Description( "Amortiguación MIENTRAS la physgun lo sostiene: deja sacudir el cadáver 'como mantel sucio' sin que los joints se desarmen, y al soltarlo vuelve a caer normal (la amortiguación fija hacía que cayera en cámara lenta). 0 = desactivado. Lo maneja el puente del juego" )]
public float AmortiguacionAgarre { get; set; } = 5f;
/// <summary>Aplica amortiguación a los cuerpos del ragdoll YA encendido.
/// La llama el puente de la physgun (la librería no conoce al arma del
/// juego): alto mientras lo zarandean, de vuelta a AmortiguacionRagdoll
/// al soltarlo para que el cadáver caiga con peso natural.</summary>
public void AplicarAmortiguacion( float lineal )
{
var fisica = Components.Get<ModelPhysics>();
if ( !fisica.IsValid() ) return;
foreach ( var cuerpo in fisica.Bodies )
{
if ( !cuerpo.Component.IsValid() ) continue;
cuerpo.Component.LinearDamping = lineal;
cuerpo.Component.AngularDamping = lineal * 2f;
}
}
[Property, Description( "Congela la animación al morir. El ragdoll se enciende SOBRE el bot vivo, así que el renderer sigue reproduciendo la secuencia y REESCRIBE los huesos cada frame peleando contra los cuerpos físicos: los huesos sin cuerpo tiemblan y, si hay pocos cuerpos, el cadáver entero flota por el mapa. Dejar en ON salvo que se necesite un cadáver animado" )]
public bool CongelarAnimAlMorir { get; set; } = true;
[Property, Description( "Este cadáver SIEMPRE se desvanece a los TiempoDespawn segundos, ignorando el interruptor global sbh.ragdolls (para colosos que no deben acumularse)" )]
public bool DespawnForzado { get; set; } = false;
protected override void OnEnabled()
{
Vida.AlMorir += Morir;
}
protected override void OnDisabled()
{
Vida.AlMorir -= Morir;
}
async void Morir()
{
// Marca pública de cadáver: otros sistemas (ritmos, integraciones)
// distinguen "muerto" sin adivinar por componentes de física
GameObject.Tags.Add( "sbh_muerto" );
if ( SonidoMuerte != null )
Sound.Play( SonidoMuerte, WorldPosition );
// 1. Apagar la IA, los sentidos, la cara y el sonido de pánico.
// La cabeza primero: debe soltar su override del hueso antes de
// que el ragdoll tome el esqueleto, o la física pelea contra él.
Apagar<CabezaNextbot>();
Apagar<SaltoNextbot>();
Apagar<NextbotBrain>();
Apagar<PercepcionNextbot>();
Apagar<NavMeshAgent>();
Apagar<PanicSound>();
var renderer = Components.Get<SkinnedModelRenderer>();
// 2. Pose de reposo ANTES del ragdoll. Solo el hueso Hips tiene cuerpo
// físico: el resto del esqueleto queda congelado en la pose del momento
// de morir. Si murió corriendo, quedaría a mitad de zancada — así que
// cambiamos a la pose de reposo (sin blending, snap instantáneo) y
// esperamos un frame a que el esqueleto la aplique.
if ( renderer.IsValid() && !string.IsNullOrEmpty( AnimReposo ) )
{
// El animgraph debe apagarse primero: Sequence solo funciona sin él,
// y el ragdoll tampoco lo quiere activo (doc oficial de UseAnimGraph)
renderer.UseAnimGraph = false;
renderer.Sequence.Blending = false;
renderer.Sequence.Name = AnimReposo;
await Task.Frame();
if ( !GameObject.IsValid() ) return;
}
// 3. Activar el ragdoll: crear apagado, configurar TODO y recién ahí
// encender. ModelPhysics necesita Model y Renderer asignados ANTES de
// habilitarse, si no, no construye ningún cuerpo físico.
var fisica = Components.Get<ModelPhysics>( true );
if ( fisica == null )
fisica = Components.Create<ModelPhysics>( false );
// Congelar la animación ANTES de encender la física: si el renderer
// sigue tickeando la secuencia, cada frame reescribe los huesos y la
// física los corrige — realimentación. Con muchos cuerpos casi no se
// nota (todos los huesos son físicos); con pocos, el cadáver flota
// por el mapa (caso Siren Head con un único cuerpo).
if ( CongelarAnimAlMorir && renderer.IsValid() )
renderer.PlaybackRate = 0f;
fisica.Enabled = false;
fisica.Renderer = renderer;
fisica.Model = renderer?.Model;
fisica.MotionEnabled = true;
fisica.StartAsleep = false;
fisica.Enabled = true;
// 4. Empujón para que se desplome en vez de quedar parado en equilibrio.
// Si Bodies queda vacío (hull sin hueso válido), el ragdoll no existe
// aunque PhysicsWereCreated diga que sí — en ese caso, plan B.
// Peso aparente (2026-07-29): un coloso no vuela — los impulsos se
// dividen por Peso (1 en rigs normales = comportamiento de siempre)
var peso = System.MathF.Max( Peso, 1f );
var cuerpos = 0;
foreach ( var cuerpo in fisica.Bodies )
{
if ( !cuerpo.Component.IsValid() ) continue;
cuerpo.Component.AngularVelocity = Vector3.Random * (FuerzaEmpujon / peso);
// Ejecución: si murió por un golpe con impulso, el cadáver vuela
if ( Vida.ImpulsoMortal != Vector3.Zero )
cuerpo.Component.Velocity = Vida.ImpulsoMortal / peso;
// Amortiguación opcional (2026-08-01, lección Siren Head): en un
// coloso los cuerpos grandes se empujan entre sí y el cadáver sale
// "de viaje" contra las paredes. Con damping la energía se disipa
// y el muñeco se acuesta. Default 0 = rigs normales sin cambios.
if ( AmortiguacionRagdoll > 0f )
{
cuerpo.Component.LinearDamping = AmortiguacionRagdoll;
cuerpo.Component.AngularDamping = AmortiguacionRagdoll * 2f;
}
cuerpos++;
}
if ( cuerpos > 0 )
{
// Ragdoll real: los colliders del bot VIVO ya no hacen falta — el
// ragdoll trae su propia física. Y si quedan encendidos PELEAN
// contra los cuerpos del ragdoll y expulsan el cadáver (caso Siren
// Head: su BoxCollider de daño de gigante lo mandaba a volar por
// el mapa; los rigs con CapsuleCollider nunca lo notaron porque
// esa sí se apagaba desde siempre).
Apagar<CapsuleCollider>();
Apagar<BoxCollider>();
Apagar<SphereCollider>();
Log.Info( $"☠️ {GameObject.Name} eliminado → ragdoll con {cuerpos} cuerpo(s)" );
}
else
{
// Plan B: ragdoll rígido con Rigidbody + la cápsula
// (que debe quedar ENCENDIDA para esto).
fisica.Destroy();
var rb = Components.GetOrCreate<Rigidbody>();
rb.AngularVelocity = Vector3.Random * (FuerzaEmpujon / peso);
// Freno anti-trompo (2026-07-27) SOLO angular. El LinearDamping
// bajó de 0.5 a 0.05 (2026-07-29): frenaba la CAÍDA y el coloso
// bajaba flotando como papel — el peso se tiene que NOTAR
rb.LinearDamping = 0.05f;
rb.AngularDamping = 2f;
if ( Vida.ImpulsoMortal != Vector3.Zero )
rb.Velocity = Vida.ImpulsoMortal / peso;
Log.Warning( $"☠️ {GameObject.Name}: el ragdoll del modelo no generó cuerpos, uso Rigidbody de emergencia" );
}
// 5. Esperar tirado en el piso y desvanecerse (0 = se queda para siempre)
if ( TiempoDespawn > 0f )
{
try
{
await Task.DelaySeconds( TiempoDespawn );
// Interruptor del menú (sbh.ragdolls): con la limpieza apagada el
// cadáver espera; si la reactivan, se desvanece al ratito.
// DespawnForzado (colosos) lo IGNORA: este cadáver se va sí o sí
while ( !DespawnForzado && !SbhConVars.LimpiarRagdolls )
{
if ( !GameObject.IsValid() ) return;
await Task.DelaySeconds( 1f );
}
}
catch ( OperationCanceledException )
{
// El cadáver fue borrado a mano (menú C, undo) a mitad de la
// espera: el engine cancela la task del componente destruido
// y el await revienta — no queda nada que desvanecer
return;
}
Desvanecer( renderer );
}
}
/// <summary>
/// Congela la física en la pose actual y desvanece el cadáver:
/// baja el alpha del tinte mientras se hunde en el piso, y al final
/// destruye el GameObject.
/// </summary>
async void Desvanecer( SkinnedModelRenderer renderer )
{
if ( !GameObject.IsValid() ) return;
// Congelar la física para que no pelee con el hundimiento,
// y dejar de chocar con el jugador
foreach ( var rb in Components.GetAll<Rigidbody>( FindMode.EverythingInSelfAndDescendants ) )
rb.MotionEnabled = false;
foreach ( var col in Components.GetAll<Collider>( FindMode.EverythingInSelfAndDescendants ) )
col.Enabled = false;
var tintInicial = renderer.IsValid() ? renderer.Tint : Color.White;
var transcurrido = 0f;
try
{
while ( transcurrido < TiempoDesvanecer )
{
if ( !GameObject.IsValid() ) return;
transcurrido += Time.Delta;
var visible = 1f - (transcurrido / TiempoDesvanecer).Clamp( 0f, 1f );
if ( renderer.IsValid() )
renderer.Tint = tintInicial.WithAlpha( visible );
// Hundirse en el piso, por si el material no soporta transparencia
WorldPosition += Vector3.Down * 25f * Time.Delta;
await Task.Frame();
}
}
catch ( OperationCanceledException )
{
// Borrado a mano a mitad del desvanecimiento: ya lo destruyó otro
return;
}
GameObject.Destroy();
}
void Apagar<T>() where T : Component
{
var c = Components.Get<T>();
if ( c != null ) c.Enabled = false;
}
}