Component that pins specified skeletal bones to the ground each frame. It reads animated bone world transforms, traces down to find the floor ignoring the bot and players, smooths Z with exponential lerp, and sets per-bone overrides on the SkinnedModelRenderer; it clears overrides when disabled or when the actor is dead/ragdolled.
namespace SBH;
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// [SBHBase] Planta huesos al suelo — el "foot planting" de los gigantes que
/// caminan con las manos o arrastran la cabeza (pedido para el Sea Eater).
/// Cada frame toma la pose ANIMADA del hueso (patrón BaileProcedural:
/// TryGetBoneTransformAnimation, sin retroalimentación), busca el piso con
/// un rayo vertical y le clava la Z al terreno (offset + suavizado).
/// NO es IK de cadena (el codo no se dobla solo): es el truco de pegar el
/// hueso terminal al suelo — para patas y cabezas que arrastran, rinde.
/// OJO: comparte ClearBoneOverrides con los otros drivers de huesos
/// (HuesosManipulados, BaileProcedural) — un solo driver por bot a la vez.
/// </summary>
public sealed class HuesosAlSuelo : Component
{
[Property, Description( "Huesos a plantar, separados por coma (ej: forearm.L_phy, forearm.R_phy)" )]
public string Huesos { get; set; } = "";
[Property, Description( "Offset sobre el piso en unidades (0 = apoyado justo; subilo si el hueso es el centro de una mano gorda)" )]
public float Altura { get; set; } = 0f;
[Property, Description( "Cuánto sube el rayo por encima del hueso antes de buscar piso" )]
public float AlturaBusqueda { get; set; } = 256f;
[Property, Description( "Cuánto puede bajar el rayo buscando piso; si no hay piso en ese tramo el hueso queda animado" )]
public float Alcance { get; set; } = 2048f;
[Property, Description( "Velocidad de acomodo (lerp exponencial); 0 = clavado duro" )]
public float Suavizado { get; set; } = 10f;
SkinnedModelRenderer _renderer;
readonly Dictionary<string, float> _zSuave = new();
bool _aplicado;
protected override void OnEnabled()
{
_renderer = Components.Get<SkinnedModelRenderer>();
_zSuave.Clear();
}
protected override void OnDisabled() => Limpiar();
protected override void OnUpdate()
{
if ( !_renderer.IsValid() || _renderer.SceneModel == null ) return;
// Muerto o ragdoll activo: la física manda, nada de plantar huesos
// (doble cerrojo: el tag puede llegar un frame tarde o vivir en el
// root — y ModelPhysics encendido es ragdoll seguro)
if ( GameObject.Tags.Has( "sbh_muerto" ) || GameObject.Root.Tags.Has( "sbh_muerto" )
|| Components.Get<ModelPhysics>() is not null )
{
Limpiar();
return;
}
var sm = _renderer.SceneModel;
sm.ClearBoneOverrides();
_aplicado = false;
foreach ( var crudo in (Huesos ?? "").Split( ',' ) )
{
var nombre = crudo.Trim();
if ( nombre == "" ) continue;
var hueso = _renderer.Model?.Bones?.GetBone( nombre );
if ( hueso == null ) continue;
if ( !_renderer.TryGetBoneTransformAnimation( hueso, out var mundo ) ) continue;
// Piso bajo el hueso, ignorando al propio bot y a los demás bichos
var piso = Scene.Trace
.Ray( mundo.Position + Vector3.Up * AlturaBusqueda,
mundo.Position + Vector3.Down * Alcance )
.IgnoreGameObjectHierarchy( GameObject.Root )
.WithoutTags( "player", "sbh_nextbot" )
.Run();
if ( !piso.Hit ) continue;
var zObjetivo = piso.EndPosition.z + Altura;
if ( Suavizado > 0f && _zSuave.TryGetValue( nombre, out var zPrevio ) )
zObjetivo = MathX.Lerp( zPrevio, zObjetivo, Time.Delta * Suavizado );
_zSuave[nombre] = zObjetivo;
mundo.Position = mundo.Position.WithZ( zObjetivo );
sm.SetBoneOverride( hueso.Index, sm.Transform.ToLocal( mundo ) );
_aplicado = true;
}
}
void Limpiar()
{
if ( !_aplicado ) return;
_aplicado = false;
_zSuave.Clear();
if ( _renderer.IsValid() && _renderer.SceneModel != null )
_renderer.SceneModel.ClearBoneOverrides();
}
}