OverdriveCorpse.cs
using Sandbox;
namespace PaintballBaddies;
public sealed class OverdriveCorpse : Component
{
private SkinnedModelRenderer renderer;
private SkinnedModelRenderer physicsRenderer;
private ModelPhysics physics;
private SkinnedModelRenderer source;
private SkinnedModelRenderer nativeSource;
private int missing;
private bool hollow;
private int headVariant=2;
private Vector3 impulse;
private bool initialized;
private float age;
private readonly List<(string Name,string Parent,float Length)> fittedJoints=new();
private readonly Dictionary<string,Transform> physicalPose=new(),fittedPose=new();
public bool Landed {get;private set;}
internal GameObject SourceEnemy {get;private set;}
internal SkinnedModelRenderer VisibleBody=>renderer;
internal static void Spawn(OverdriveRobotRig rig,int parts,Vector3 force)
{
if(rig.Scene.GetAllComponents<OverdriveCorpse>().Any(c=>c.SourceEnemy==rig.GameObject))return;
foreach(var old in rig.Scene.GetAllComponents<OverdriveCorpse>().OrderBy(x=>x.age).Skip(5).ToArray())old.GameObject.Destroy();
var go=new GameObject(false,"Defeated grumpy sausage ragdoll"){NetworkMode=NetworkMode.Never,WorldTransform=rig.Body.WorldTransform};
go.Tags.Add("paintball_debris");
var corpse=go.Components.Create<OverdriveCorpse>();corpse.SourceEnemy=rig.GameObject;corpse.missing=parts;corpse.impulse=force;corpse.source=rig.Body;corpse.nativeSource=rig.Body;
corpse.hollow=rig.Components.Get<OverdriveEnemy>()?.HollowTorso==true;
corpse.headVariant=rig.Components.Get<OverdriveEnemy>()?.HeadBreakVariant ?? 2;
corpse.renderer=go.Components.Create<SkinnedModelRenderer>();corpse.renderer.Model=rig.Body.Model;
corpse.renderer.UseAnimGraph=false;corpse.renderer.Tint=rig.Body.Tint;
corpse.renderer.RenderOptions.Game=false;
var driver=new GameObject(go,true,"Ragdoll skeleton"){NetworkMode=NetworkMode.Never};
corpse.physicsRenderer=driver.Components.Create<SkinnedModelRenderer>();corpse.physicsRenderer.Model=rig.Body.Model;
corpse.physicsRenderer.UseAnimGraph=false;corpse.physicsRenderer.RenderOptions.Game=false;corpse.physicsRenderer.RenderType=ModelRenderer.ShadowRenderType.Off;
// Keep physics on its own renderer. Constraint separation on impact
// must not stretch the visible sausage's short fitted neck and limbs.
var pending=rig.Body.Model.Bones.AllBones.ToList();
while(pending.Count>0)
{
var bone=pending.FirstOrDefault(b=>b.Parent is null || corpse.fittedJoints.Any(j=>j.Name==b.Parent.Name));
if(bone is null)break;
float length=bone.Parent is null ? 0 : (rig.Body.Model.GetBoneTransform(bone.Name).Position-rig.Body.Model.GetBoneTransform(bone.Parent.Name).Position).Length;
corpse.fittedJoints.Add((bone.Name,bone.Parent?.Name,length));
pending.Remove(bone);
}
corpse.physics=go.Components.Create<ModelPhysics>();corpse.physics.Model=rig.Body.Model;corpse.physics.Renderer=corpse.physicsRenderer;
corpse.physics.MotionEnabled=false;
go.Enabled=true;
}
protected override void OnUpdate()
{
age+=Time.Delta;
if(!initialized && physics.Bodies?.Count>0)
{
// Use the engine's normal ragdoll handoff from the intact animated
// renderer. Never apply fitted skin overrides to the physics driver;
// ModelPhysics remains the sole owner of its simulated skeleton.
if(!nativeSource.IsValid()){GameObject.Destroy();return;}
physics.CopyBonesFrom(nativeSource,true);
physics.MotionEnabled=true;renderer.RenderOptions.Game=true;
if(source.IsValid()){source.RenderOptions.Game=false;source.RenderType=ModelRenderer.ShadowRenderType.Off;}
foreach(var body in physics.Bodies)if(body.Component.IsValid())
{
body.Component.Velocity=impulse.Normal*((missing&1)!=0 ? 45 : 110)+Vector3.Up*4;
body.Component.AngularVelocity=new Vector3(0,.8f,0);body.Component.AngularDamping=1.2f;
}
var chest=source.IsValid() && source.TryGetBoneTransform("spine_2",out var chestPose) ? chestPose.Position : WorldPosition+Vector3.Up*40;
foreach(var body in physics.Bodies.Where(b=>b.Component.IsValid()).OrderBy(b=>(b.Component.WorldPosition-chest).Length).Take(2))
body.Component.Components.Create<OverdriveLandingContact>().Corpse=this;
initialized=true;
}
if(age>4)GameObject.Destroy();
}
internal void Land(Vector3 point)
{
if(Landed || age<.12f)return;
Landed=true;OverdriveAudio.Play("landing",point);
}
protected override void OnPreRender()
{
if(!initialized || !renderer.IsValid() || !physicsRenderer.IsValid())return;
physicalPose.Clear();fittedPose.Clear();
foreach(var joint in fittedJoints)
{
if(!physicsRenderer.TryGetBoneTransform(joint.Name,out var world))return;
physicalPose[joint.Name]=renderer.WorldTransform.ToLocal(world);
}
foreach(var joint in fittedJoints)
{
var pose=physicalPose[joint.Name];
if(joint.Parent is not null)
{
var delta=pose.Position-physicalPose[joint.Parent].Position;
if(delta.Length<.0001f)
{
var local=renderer.Model.GetBoneTransform(joint.Parent).ToLocal(renderer.Model.GetBoneTransform(joint.Name));
delta=physicalPose[joint.Parent].Rotation*local.Position;
}
pose.Position=fittedPose[joint.Parent].Position+delta.Normal*joint.Length;
}
fittedPose[joint.Name]=pose;
}
foreach(var bone in renderer.Model.Bones.AllBones)
renderer.SetBoneTransform(bone,fittedPose[bone.Name]);
OverdriveRobotRig.ApplyParts(renderer,missing,hollow:hollow,headVariant:headVariant);
}
}
public sealed class OverdriveLandingContact : Component, Component.ICollisionListener
{
internal OverdriveCorpse Corpse {get;set;}
void Component.ICollisionListener.OnCollisionStart(Collision collision)
{if(Corpse.IsValid())Corpse.Land(WorldPosition);}
}