ReloadUpperBodyData.cs
using Sandbox;
using System;
using System.Collections.Generic;
namespace PaintballBaddies;
/// <summary>Offline sampled reload transforms; rotations preserve native bind axes.</summary>
public static class ReloadUpperBodyData
{
public sealed class BonePose { public float[] position {get;set;} public float[] rotationDelta {get;set;} }
public sealed class Frame { public BonePose[] bones {get;set;} }
public sealed class Track { public int version {get;set;} public float duration {get;set;} public string[] names {get;set;} public Frame[] frames {get;set;} }
private static readonly Dictionary<string,Track> cache=new();
public static float BlendWeight(float phase)
{
var weight=MathF.Min((phase/.08f).Clamp(0,1),((1-phase)/.08f).Clamp(0,1));
return weight*weight*(3-2*weight);
}
public static Vector3 MarkerOffset(string modelPath,Vector3 carryOffset,float phase)
{
var authored=CoveredRosterAssets.CharacterIndex(modelPath)==2 ? new Vector3(4.92126f,-.984252f,-3.740157f) : carryOffset;
return Vector3.Lerp(carryOffset,authored,BlendWeight(phase));
}
internal static string ImaniReviewTrack {get;set;}
public static Track Load(string character)
{
var path=character=="imani" ? (string.IsNullOrEmpty(ImaniReviewTrack) ? "animations/imani_reload_reach.json" : ImaniReviewTrack) : $"animations/{character}_reload_upper_body.json";
if(!cache.TryGetValue(path,out var track))
{
track=Json.Deserialize<Track>(FileSystem.Mounted.ReadAllText(path));
cache[path]=track;
}
return track;
}
public static Transform Sample(Track track,Model model,int boneIndex,float phase)
{
var sample=phase.Clamp(0,1)*(track.frames.Length-1);var i=(int)sample;var t=sample-i;
var a=track.frames[i].bones[boneIndex];var b=track.frames[Math.Min(i+1,track.frames.Length-1)].bones[boneIndex];
var p=Vector3.Lerp(new(a.position[0],a.position[1],a.position[2]),new(b.position[0],b.position[1],b.position[2]),t);
var qa=new Rotation(a.rotationDelta[0],a.rotationDelta[1],a.rotationDelta[2],a.rotationDelta[3]);
var qb=new Rotation(b.rotationDelta[0],b.rotationDelta[1],b.rotationDelta[2],b.rotationDelta[3]);
return new Transform(p,Rotation.Slerp(qa,qb,t)*model.GetBoneTransform(track.names[boneIndex]).Rotation);
}
public static bool Apply(SkinnedModelRenderer body,float phase,Vector3 markerOffset,out Transform markerPose,out Vector3 bodyOffset,bool writeBones=true)
{
markerPose=default;bodyOffset=default;
var index=CoveredRosterAssets.CharacterIndex(body.Model?.Name);
if(index<0)return false;
var track=Load(RosterSelection.Names[index].ToLowerInvariant());
var spineIndex=Array.IndexOf(track.names,"Spine");
var spine=body.Model.Bones.GetBone("Spine");
if(spineIndex<0 || !body.TryGetBoneTransformAnimation(spine,out var nativeSpine))return false;
bodyOffset=body.WorldTransform.PointToLocal(nativeSpine.Position)-Sample(track,body.Model,spineIndex,phase).Position;
var weight=BlendWeight(phase);
// Imani's reload prop track uses the original marker mount. Blend that
// authored mount with the calibrated carry mount over the same pose fade.
var blendedMarkerOffset=MarkerOffset(body.Model?.Name,markerOffset,phase);
for(int i=0;i<track.names.Length;i++)
{
var bone=body.Model.Bones.GetBone(track.names[i]);
if(!body.TryGetBoneTransformAnimation(bone,out var native))continue;
var current=body.WorldTransform.ToLocal(native);var target=Sample(track,body.Model,i,phase);
var pose=new Transform(Vector3.Lerp(current.Position,target.Position+bodyOffset,weight),Rotation.Slerp(current.Rotation,target.Rotation,weight));
if(writeBones)body.SetBoneTransform(bone,pose);
if(track.names[i]=="RightHand")markerPose=new Transform(body.WorldTransform.PointToWorld(pose.Position+blendedMarkerOffset),body.WorldRotation);
}
return true;
}
}