Component that applies per-bone manual adjustments (angles, position, scale) on top of evaluated animation for a skinned model. It stores named bone adjustments, applies them each update by reading the animation pose and setting bone overrides on the SceneModel, and clears overrides when disabled or no adjustments present.
namespace SBH;
using Sandbox;
using System.Collections.Generic;
/// <summary>
/// [SBHBase] Huesos manipulados a mano (los ManipulateBone* de GMod, para el
/// Advanced Bone Tool): guarda un ajuste por hueso (ángulos/posición/escala,
/// offsets en el MARCO LOCAL del hueso) y lo aplica cada frame POR ENCIMA de
/// la animación ya evaluada — patrón BaileProcedural: TryGetBoneTransformAnimation
/// lee la pose SIN overrides (sin retroalimentación) y SetBoneOverride pisa.
/// Con la lista vacía no toca nada (la animación queda pura). En un bot
/// muerto (tag sbh_muerto) se apaga: el ragdoll manda.
/// OJO: comparte SceneModel.ClearBoneOverrides con otros drivers de huesos
/// (ej. BaileProcedural) — dos drivers vivos en el mismo bot se pisan entre sí.
/// </summary>
public sealed class HuesosManipulados : Component
{
public struct Ajuste
{
public Angles Angulos;
public Vector3 Posicion;
public float Escala;
public static readonly Ajuste Identidad = new() { Escala = 1f };
public bool EsIdentidad =>
Angulos == Angles.Zero && Posicion == Vector3.Zero && Escala == 1f;
}
readonly Dictionary<string, Ajuste> _ajustes = new();
SkinnedModelRenderer _renderer;
bool _aplicado;
public IReadOnlyDictionary<string, Ajuste> Ajustes => _ajustes;
public Ajuste De( string hueso )
=> hueso != null && _ajustes.TryGetValue( hueso, out var a ) ? a : Ajuste.Identidad;
public void Fijar( string hueso, Ajuste ajuste )
{
if ( string.IsNullOrEmpty( hueso ) ) return;
if ( ajuste.EsIdentidad )
_ajustes.Remove( hueso );
else
_ajustes[hueso] = ajuste;
}
public void Reiniciar() => _ajustes.Clear();
protected override void OnEnabled()
{
_renderer = Components.Get<SkinnedModelRenderer>();
}
protected override void OnDisabled() => Limpiar();
protected override void OnUpdate()
{
if ( !_renderer.IsValid() || _renderer.SceneModel == null ) return;
if ( _ajustes.Count == 0 || GameObject.Tags.Has( "sbh_muerto" ) )
{
Limpiar();
return;
}
var sm = _renderer.SceneModel;
sm.ClearBoneOverrides();
foreach ( var (nombre, ajuste) in _ajustes )
{
var hueso = _renderer.Model?.Bones?.GetBone( nombre );
if ( hueso == null ) continue;
// Pose de la ANIMACIÓN (mundo), sin nuestros overrides
if ( !_renderer.TryGetBoneTransformAnimation( hueso, out var mundo ) ) continue;
var local = sm.Transform.ToLocal( mundo );
// Offsets en el marco local del hueso (como ManipulateBone* de GMod)
local.Position += local.Rotation * ajuste.Posicion;
local.Rotation *= Rotation.From( ajuste.Angulos );
local.Scale *= ajuste.Escala;
sm.SetBoneOverride( hueso.Index, local );
}
_aplicado = true;
}
void Limpiar()
{
if ( !_aplicado ) return;
_aplicado = false;
if ( _renderer.IsValid() && _renderer.SceneModel != null )
_renderer.SceneModel.ClearBoneOverrides();
}
}