ReloadPresentationTrack.cs
using Sandbox;
namespace PaintballBaddies;
/// <summary>Plays the authored reload presentation on an isolated character candidate.</summary>
public sealed class ReloadPresentationTrack : Component
{
public SkinnedModelRenderer Body { get; set; }
public float Phase { get; set; }
public bool Docked { get; set; }
public Vector3 BodyOffset { get; set; }
public string TrackPath { get; set; } = "animations/roxie_reload_presentation.json";
public sealed class Pose { public float[] position { get; set; } public float[] rotation { get; set; } public bool visible { get; set; } }
public sealed class Frame { public Pose[] objects { get; set; } public float leftGrip { get; set; } public float leftThumb { get; set; } }
public sealed class Track { public float duration { get; set; } public Frame[] frames { get; set; } public Pose[] dockObjects { get; set; } public float[] dockBoneRestRotation { get; set; } }
private Track track;
private readonly System.Collections.Generic.List<GameObject> props = new();
public System.Collections.Generic.IReadOnlyList<GameObject> PresentationProps => props;
protected override void OnStart()
{
track = Json.Deserialize<Track>( FileSystem.Mounted.ReadAllText( TrackPath ) );
for ( int i = 0; i < 11; i++ )
{
var prop = new GameObject { Name = $"Reload presentation {i}", NetworkMode=NetworkMode.Never };
var renderer = prop.Components.Create<ModelRenderer>();
renderer.Model = Model.Load( i < 3 ? $"models/gear/refill_pod/{new[] { "body", "lid", "pouch" }[i]}.vmdl" : "models/dev/sphere.vmdl" );
if ( i >= 3 ) { prop.WorldScale = new Vector3( .0063f ); renderer.Tint = new Color( .95f, .28f, .025f ); }
props.Add( prop );
}
}
protected override void OnUpdate()
{
ApplyFrame();
}
public void ApplyFrame()
{
if ( track is null || !Body.IsValid() ) return;
if ( Docked )
{
if ( track.dockObjects is null || !Body.TryGetBoneTransform( "Hips", out var hip ) ) return;
var rotation = hip.Rotation;
if ( track.dockBoneRestRotation is { Length: 4 } r )
rotation *= Body.Model.GetBoneTransform( "Hips" ).Rotation.Inverse * new Rotation( r[0], r[1], r[2], r[3] );
for ( int i = 0; i < props.Count; i++ )
{
props[i].Enabled = i < track.dockObjects.Length;
if ( i >= track.dockObjects.Length ) continue;
var pose = track.dockObjects[i];
props[i].WorldPosition = hip.Position + rotation * new Vector3( pose.position[0], pose.position[1], pose.position[2] );
props[i].WorldRotation = rotation * new Rotation( pose.rotation[0], pose.rotation[1], pose.rotation[2], pose.rotation[3] );
}
foreach ( var name in Body.Morphs.Names )
if ( name == "LeftGrip" || name == "LeftThumb" || name == "RightGrip" || name == "RightThumb" ) Body.Morphs.Set( name, 1, 0 );
return;
}
float sample = System.Math.Clamp( Phase, 0, 1 ) * (track.frames.Length - 1);
int index = (int)sample; var a = track.frames[index]; var b = track.frames[System.Math.Min( index + 1, track.frames.Length - 1 )]; float t = sample - index;
for ( int i = 0; i < props.Count; i++ )
{
var x = a.objects[i]; var y = b.objects[i];
var p = Vector3.Lerp( new Vector3( x.position[0], x.position[1], x.position[2] ), new Vector3( y.position[0], y.position[1], y.position[2] ), t );
var q = Rotation.Slerp( new Rotation( x.rotation[0], x.rotation[1], x.rotation[2], x.rotation[3] ), new Rotation( y.rotation[0], y.rotation[1], y.rotation[2], y.rotation[3] ), t );
props[i].WorldPosition = Body.WorldPosition + Body.WorldRotation * (p + BodyOffset);
props[i].WorldRotation = Body.WorldRotation * q;
props[i].Enabled = x.visible;
}
// The authored track already interpolates grip; an additional animation-time
// fade also prevents the shape reaching its target in a paused pose review.
Body.Morphs.Set( "LeftGrip", a.leftGrip + (b.leftGrip - a.leftGrip) * t, 0 );
Body.Morphs.Set( "LeftThumb", a.leftThumb + (b.leftThumb - a.leftThumb) * t, 0 );
Body.Morphs.Set( "RightGrip", 1, 0 ); Body.Morphs.Set( "RightThumb", 1, 0 );
}
protected override void OnDestroy() { foreach ( var prop in props ) prop?.Destroy(); }
}