HuesosAlSuelo.cs

Component that plants specified bones onto the ground each frame. It reads animated bone transforms, raytraces down to find floor, smooths Z with exponential lerp, and applies bone overrides on the renderer; it clears overrides when disabled or when ragdoll/dead.

NetworkingFile Access
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();
	}
}