Component for nextbot entities that allows the physgun to pick them up without breaking their AI. When prepared it suspends AI components, creates a temporary Rigidbody (motion enabled, impact damage off, pitch/roll locked), drives a fall boolean on the skinned model while suspended, and restores AI and navmesh agent once the bot lands or is released. Handles death while suspended by destroying the Rigidbody so ragdoll physics can take over.
namespace SBH;
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// [SBHBase] Permite agarrar al nextbot con la physgun sin romperle la IA.
/// El problema histórico: la physgun solo agarra Rigidbody con MotionEnabled,
/// pero un Rigidbody PERMANENTE pelea con el NavMeshAgent aunque nadie lo
/// agarre (bots atorados en el aire — feature eliminada 2026-07-16). La
/// solución: el Rigidbody se crea RECIÉN cuando la physgun mira al bot
/// (gancho en Physgun.cs → PrepararSiEsNextbot) y se destruye al aterrizar,
/// devolviéndole la IA en el lugar donde cayó.
/// Mientras está suspendido reproduce la anim "fall" vía el animgraph
/// compartido (parámetro b_fall) y el Rigidbody sale sin daño por impacto
/// (EnableImpactDamage = false — la physgun ya no lastima).
/// El componente se crea solo bajo demanda: los prefabs quedan limpios.
/// </summary>
public sealed class AgarreNextbot : Component
{
[Property, Description( "Segundos sin que la physgun lo mire/sostenga antes de devolverle la IA (si además está en el piso)" )]
public float Gracia { get; set; } = 0.6f;
[Property, Description( "Velocidad por debajo de la cual se considera que aterrizó" )]
public float VelocidadReposo { get; set; } = 80f;
[Property, Description( "Nombre del parámetro bool del animgraph para la anim de caída" )]
public string ParametroCaida { get; set; } = "b_fall";
bool _suspendido;
bool _fueAgarrado; // pasó de hover a agarre real
TimeSince _interes; // desde la última vez que la physgun lo miró
TimeSince _sostenido; // desde el último frame sostenido/tironeado
TimeSince _quieto; // desde que está lento y apoyado
Rigidbody _rb;
Vida _vida;
SkinnedModelRenderer _renderer;
readonly List<Component> _apagados = new();
// ---------- API estática (la llama el parche de Physgun.cs) ----------
/// <summary>
/// La physgun está mirando este objeto: si es un nextbot vivo, prepararlo
/// (Rigidbody al vuelo + IA suspendida). Un frame después ya es agarrable.
/// </summary>
public static void PrepararSiEsNextbot( GameObject go )
{
if ( go == null || !go.IsValid() ) return;
var root = go.Root;
if ( !root.IsValid() ) return;
if ( root.Components.Get<NextbotBrain>( true ) == null ) return;
// Cadáveres no: el ragdoll de MuerteNextbot es quien manda ahí
var vida = root.Components.Get<Vida>();
if ( vida.IsValid() && vida.EstaMuerto ) return;
if ( root.Components.Get<ModelPhysics>().IsValid() ) return;
root.Components.GetOrCreate<AgarreNextbot>().Preparar();
}
/// <summary> La physgun lo está sosteniendo o tironeando este frame. </summary>
public static void MarcarSostenido( GameObject go )
{
if ( go == null || !go.IsValid() ) return;
go.Root?.Components.Get<AgarreNextbot>()?.RegistrarSostenido();
}
// ---------- Ciclo ----------
protected override void OnEnabled()
{
_vida = Components.Get<Vida>();
if ( _vida.IsValid() )
_vida.AlMorir += MuerteEnElAire;
}
protected override void OnDisabled()
{
if ( _vida.IsValid() )
_vida.AlMorir -= MuerteEnElAire;
}
void Preparar()
{
_interes = 0f;
if ( _suspendido ) return;
_suspendido = true;
_fueAgarrado = false;
_quieto = 0f;
// Suspender la IA, recordando SOLO lo que estaba encendido
_apagados.Clear();
Apagar<CabezaNextbot>();
Apagar<SaltoNextbot>();
Apagar<NextbotBrain>();
Apagar<NavMeshAgent>();
// El Rigidbody usa la cápsula existente como forma. Sin daño por
// impacto: que revolear al bot no lo lastime (ni a lo que golpea).
// Pitch/roll BLOQUEADOS: el secreto de estabilidad de los personajes
// oficiales — la cápsula no puede volcarse, así que siempre cae
// parado (el yaw queda libre para girarlo con la physgun)
_rb = Components.GetOrCreate<Rigidbody>();
_rb.MotionEnabled = true;
_rb.EnableImpactDamage = false;
_rb.Locking = new PhysicsLock { Pitch = true, Roll = true };
// La anim de caída NO se activa acá: OnFixedUpdate la liga a la
// realidad (colgando o en el aire) — un hover con el bot apoyado
// en el piso debe seguir viéndose en idle
_renderer = Components.Get<SkinnedModelRenderer>();
}
void RegistrarSostenido()
{
_sostenido = 0f;
_interes = 0f;
_fueAgarrado = true;
}
protected override void OnFixedUpdate()
{
if ( !_suspendido ) return;
var enPiso = EnPiso();
// ¿Sigue en manos de la physgun? (agarrado, o apenas mirado de hover)
var enManos = _fueAgarrado ? _sostenido < 0.15f : _interes < Gracia;
// La anim de caída refleja la REALIDAD: colgando de la physgun o en
// el aire. Parado en el piso — aunque siga suspendido — es idle
_renderer?.Set( ParametroCaida, (_fueAgarrado && enManos) || !enPiso );
if ( enManos )
{
_quieto = 0f;
return;
}
// Suelto: esperar el aterrizaje para devolver la IA
if ( _rb.IsValid() && _rb.MotionEnabled && _rb.Velocity.Length > VelocidadReposo )
{
_quieto = 0f;
return;
}
// Congelado (o quieto) en el aire = estatua: queda suspendido ahí
if ( !enPiso )
{
_quieto = 0f;
return;
}
if ( _quieto < 0.1f ) return;
Restaurar();
}
void Restaurar()
{
_suspendido = false;
if ( _rb.IsValid() ) _rb.Destroy();
_rb = null;
// Enderezar por las dudas: solo yaw, sin ninguna inclinación residual
WorldRotation = Rotation.FromYaw( WorldRotation.Yaw() );
_renderer?.Set( ParametroCaida, false );
foreach ( var c in _apagados )
{
if ( c.IsValid() ) c.Enabled = true;
}
_apagados.Clear();
// Re-anclar el agente al navmesh donde cayó (si quedó fuera, el agente
// lo acomoda al punto navegable más cercano)
Components.Get<NavMeshAgent>()?.SetAgentPosition( WorldPosition );
}
/// <summary>
/// Murió mientras estaba suspendido (un tiro en el aire): nuestro
/// Rigidbody debe desaparecer ANTES de que MuerteNextbot arme el ragdoll,
/// o la física pelea. La IA queda apagada — el muerto no camina.
/// </summary>
void MuerteEnElAire()
{
if ( !_suspendido ) return;
_suspendido = false;
_apagados.Clear();
if ( _rb.IsValid() ) _rb.Destroy();
_rb = null;
_renderer?.Set( ParametroCaida, false );
}
bool EnPiso()
{
var tr = Scene.Trace.Ray( WorldPosition + Vector3.Up * 4f, WorldPosition + Vector3.Down * 12f )
.IgnoreGameObjectHierarchy( GameObject )
.Run();
return tr.Hit;
}
void Apagar<T>() where T : Component
{
var c = Components.Get<T>();
if ( c.IsValid() && c.Enabled )
{
c.Enabled = false;
_apagados.Add( c );
}
}
}