Component for nextbots that allows the physgun to pick them up without breaking their AI. It creates a temporary Rigidbody when the physgun looks at a live nextbot, suspends AI components, plays a fall animation while suspended, and restores AI and navmesh position when the bot lands or after a grace period.
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 );
}
}
}