BaddieWardrobe.cs
using Sandbox;
using System.Linq;
namespace PaintballBaddies;
/// <summary>Complete Hi3D visuals using the existing native player and opponent animations.</summary>
public sealed class BaddieWardrobe : Component
{
private SkinnedModelRenderer driver;
private Model originalDriver;
private int selected=-1;
private string requestedPath;
private string requestedClothing;
private ClothingContainer clothing;
public bool Ready {get;private set;}
public SkinnedModelRenderer Visual {get;private set;}
public string[] Models {get;private set;}=System.Array.Empty<string>();
public void Present(SkinnedModelRenderer animationDriver,int index,bool visible)
{
if(!animationDriver.IsValid())return;
if(driver!=animationDriver){Clear();driver=animationDriver;originalDriver=driver.Model;}
bool opponent=GameObject.Parent.Components.Get<CitizenOpponentPresentation>().IsValid();
string path=visible ? BaddieWardrobeCatalog.ModelPath(index,opponent) : null;
var items=visible ? BaddieWardrobeCatalog.ActiveLook(index)?.ClothingItems : null;
string clothingKey=items is null ? "" : string.Join("|",items.Select(c=>c?.Asset ?? "missing"));
if(visible && (selected!=index || requestedPath!=path || requestedClothing!=clothingKey))Build(index,opponent,path,clothingKey);
if(!visible && selected>=0){Clear();if(originalDriver.IsValid())driver.Model=originalDriver;}
}
private void Build(int index,bool opponent,string path,string clothingKey)
{
Clear();
// Keep the fitted joints, with skin weights matched to their new elbow
// positions so bent arms do not pinch into angular wedges.
var animation=Model.Load(path);
if(!animation.IsValid() || animation.IsError)
animation=Model.Load(BaddieWardrobeCatalog.DefaultModel(index,opponent));
if(!animation.IsValid() || animation.IsError)return;
// Use the fitted skeleton's bind pose while retaining every existing
// animation graph parameter and native hand/foot IK constraint. Contact
// and vault clips retain this character's joint lengths and floor contact.
if(driver.Model!=animation)driver.Model=animation;
var items=BaddieWardrobeCatalog.ActiveLook(index)?.ClothingItems;
var garments=items?.Select(c=>string.IsNullOrWhiteSpace(c?.Asset) ? null : ResourceLibrary.Get<Clothing>(c.Asset)).ToArray();
if(garments is {Length:>0} && garments.All(c=>c is not null))
{
clothing=new ClothingContainer {PrefersHuman=true};
foreach(var item in garments)clothing.Add(item);
// Native clothing owns layering, bodygroups and bone-merged children.
// Garments must be pre-fitted; this is not an automatic body reshaper.
clothing.Apply(driver);
}
Visual=driver;
Models=new[]{animation.Name};selected=index;requestedPath=path;requestedClothing=clothingKey;Ready=true;
}
private void Clear()
{
if(clothing is not null && driver.IsValid())clothing.Reset(driver);
clothing=null;requestedClothing=null;
Visual=null;
Ready=false;selected=-1;requestedPath=null;Models=System.Array.Empty<string>();
}
// The hierarchy owns the garments. Resetting the clothing during teardown
// would write bodygroups/morphs on a renderer already being destroyed.
protected override void OnDestroy()
{
clothing=null;driver=null;originalDriver=null;Visual=null;
Ready=false;Models=System.Array.Empty<string>();
}
}