OverdriveRobotRig.cs
using Sandbox;
using Sandbox.Citizen;
using System;
namespace PaintballBaddies;

// Reuse native Citizen motion, retargeting rotation onto the sausage rest rig.
public sealed class OverdriveRobotRig : Component
{
    public SkinnedModelRenderer Body {get;private set;}
    private SkinnedModelRenderer source;
    private ModelHitboxes hitboxes;
    private SkinnedModelRenderer currentMotion;
    internal SkinnedModelRenderer MotionSource=>currentMotion.IsValid() ? currentMotion : source;
    private CitizenAnimationHelper animation;
    private OverdriveEnemy enemy;
    private Vector3 previous,smoothVelocity;
    private readonly Dictionary<string,Transform> poses=new();
    private bool fallen,attacking;
    private GameObject heldProp;
    private bool heldPlate;
    private bool heldEgg;
    private GrumpySausagePose retarget;
    private int reactionSerial;
    private int previousParts;
    private int appliedParts=-1,appliedHead=-1;
    private bool appliedHollow;
    private float hurtTime,hurtDuration=.4f;
    private float recoilTime,meleeFollowThrough;
    private float blinkDelay,blinkAge=-1;
    private float anger,rageFlush;
    // Presentation-only life: head tracking, gait lean and cartoon squash.
    private float lookRefresh,squashTime,lean,bank,previousYaw,idleClock;
    private Vector3 lookPoint;
    private bool hasLookPoint;
    public float BlinkWeight {get;private set;}
    private SkinnedModelRenderer slipSource;
    private int shownSlip;
    // Carried plate stack (off hand) and rage presentation.
    private GameObject stack;
    private int shownPlates=-1,shownStackBreak;
    private bool shownRage;
    private float puffClock;
    internal Vector3? StackCenter {get;private set;}
    internal Vector3 ThrowOrigin
    {
        get
        {
            string bone=(enemy.MissingParts&4)==0 ? "hand_R" : "hand_L";
            // Apply the latest local animation pose to the current actor transform;
            // cached world bones can still be at the previous frame's position.
            if(Body.IsValid() && retarget is not null && poses.ContainsKey(bone))
            {
                string side=(enemy.MissingParts&4)==0 ? "R" : "L";
                return Body.WorldTransform.PointToWorld(retarget.PalmCenter(side))
                    +Body.WorldRotation*retarget.PalmFrame(side).Up*(enemy.ThrowEgg ? 1.3f : 0);
            }
            return enemy.AimPoint+WorldRotation.Forward*24;
        }
    }
    internal Transform IntactPose(string bone)
    {
        // Preserve the latest fitted pose through damage/bodygroup changes.
        // Reapply the current actor transform to avoid one-frame attachment lag.
        if(Body.IsValid())return Body.WorldTransform.ToWorld(poses.TryGetValue(bone,out var pose) ? pose : Body.Model.GetBoneTransform(bone));
        return WorldTransform;
    }
    protected override void OnStart(){enemy=Components.Get<OverdriveEnemy>();_=OverdriveAssets.Load();if(OverdriveAssets.Ready)Initialize();}
    private void Initialize()
    {
        // A queued Start can arrive after a short-lived spawn was removed.
        // Do not build a visual rig for a component that is already gone.
        if(!GameObject.IsValid())return;
        enemy=Components.Get<OverdriveEnemy>();if(!enemy.IsValid())return;
        previous=WorldPosition;
        // Configure the render hierarchy before exposing it to the scene. A
        // joining client can create several enemies in the same frame.
        var visual=new GameObject(GameObject,false,"Grumpy sausage raider"){NetworkMode=NetworkMode.Never};
        Body=visual.Components.Create<SkinnedModelRenderer>();Body.Model=Model.Load(enemy.Hefty ? OverdriveAssets.HeftyPath : OverdriveAssets.RobotPath);Body.UseAnimGraph=false;
        appliedParts=appliedHead=-1;
        var driver=new GameObject(GameObject,false,"Native Citizen motion driver"){NetworkMode=NetworkMode.Never};
        source=driver.Components.Create<SkinnedModelRenderer>();source.Model=Model.Load(OverdriveAssets.NativeTerryPath);
        source.RenderOptions.Game=false;source.RenderType=ModelRenderer.ShadowRenderType.Off;source.UseAnimGraph=true;
        hitboxes=visual.Components.Create<ModelHitboxes>();hitboxes.Renderer=Body;hitboxes.Target=GameObject;
        retarget=new GrumpySausagePose(Body.Model,source.Model);
        animation=driver.Components.Create<CitizenAnimationHelper>();animation.Target=source;animation.IsGrounded=true;
        animation.HoldType=CitizenAnimationHelper.HoldTypes.Punch;
        var slipDriver=new GameObject(GameObject,false,"Native slip motion driver"){NetworkMode=NetworkMode.Never};
        slipSource=slipDriver.Components.Create<SkinnedModelRenderer>();slipSource.Model=Model.Load("models/sausage/terry/slip_driver.vmdl");
        slipSource.RenderOptions.Game=false;slipSource.RenderType=ModelRenderer.ShadowRenderType.Off;slipSource.UseAnimGraph=false;slipSource.PlaybackRate=0;
        shownSlip=enemy.SlipSerial;shownStackBreak=enemy.StackBreakSerial;shownRage=enemy.Enraged;
        reactionSerial=enemy.ReactionSerial;
        previousParts=enemy.MissingParts;
        blinkDelay=Game.Random.Float(1.2f,4.8f);
        driver.Enabled=true;slipDriver.Enabled=true;visual.Enabled=true;
    }
    protected override void OnUpdate()
    {
        if(!enemy.IsValid()){enemy=Components.Get<OverdriveEnemy>();if(!enemy.IsValid())return;}
        if(!Body.IsValid()){if(OverdriveAssets.Ready)Initialize();else return;}
        if(!Body.IsValid())return;
        if(enemy.IceShattered){Body.Enabled=false;if(stack.IsValid())stack.Enabled=false;if(heldProp.IsValid())heldProp.Enabled=false;return;}
        if(enemy.FrozenRemaining>0){source.PlaybackRate=0;previous=WorldPosition;return;}
        if(shownSlip!=enemy.SlipSerial)
        {shownSlip=enemy.SlipSerial;KitchenCombatAudio.Play("egg",WorldPosition);SausageAudio.Play("sputter",WorldPosition);FeelAudio.Play("slip_whistle",2,WorldPosition,.7f,1000,.06f);}
        if(shownStackBreak!=enemy.StackBreakSerial)
        {
            shownStackBreak=enemy.StackBreakSerial;
            var at=StackCenter ?? enemy.StackBreakPoint;
            if(shownKind==2){YolkBurst.Spawn(at);YolkBurst.Spawn(at+Vector3.Up*3);KitchenCombatAudio.Play("egg",at);SausageAudio.Play("sputter",at);}
            else{SausageCeramicBurst.Spawn(at);SausageCeramicBurst.Spawn(at+Vector3.Up*4);SausageKitchenAudio.Play(true,at);}
        }
        if(enemy.Enraged && !shownRage){shownRage=true;FeelAudio.Play("rage",3,enemy.AimPoint,.85f,1200,.05f);}
        if(enemy.Enraged && !enemy.Dead)
        {
            puffClock-=Time.Delta;
            if(puffClock<=0 && Body.IsValid())
            {
                puffClock=.34f;var head=IntactPose("head");
                var side=Game.Random.Int(0,1)==0 ? head.Rotation.Left : head.Rotation.Right;
                RagePuff.Spawn(Scene,head.Position+Vector3.Up*9+side*6,side);
            }
        }
        UpdateStack();
        rageFlush+=((enemy.Enraged && !enemy.Dead ? 1 : 0)-rageFlush)*(1-MathF.Exp(-4*Time.Delta));
        // Advance blending once per simulation frame, never once per camera.
        float carryTarget=enemy.Carrying && !enemy.Dead && enemy.SlipRemaining<=0 && (enemy.MissingParts&6)!=6 ? 1 : 0;
        carryWeight+=(carryTarget-carryWeight)*(1-MathF.Exp(-10*Time.Delta));
        bool winding=enemy.RangedWindup && enemy.Windup>0 && !enemy.Dead && enemy.SlipRemaining<=0;
        bool following=!enemy.RangedWindup && enemy.ThrowRecovery>0 && !enemy.Dead && enemy.SlipRemaining<=0;
        if(winding){throwPhase=1-enemy.Windup/OverdriveEnemy.ThrowWindup;throwFrisbee=enemy.ThrowPlate;}
        else if(following)throwPhase=1+(1-enemy.ThrowRecovery/.6f)*1.5f;
        float throwTarget=winding || (following && throwPhase<2.1f) ? 1 : 0;
        throwWeight+=(throwTarget-throwWeight)*(1-MathF.Exp(-(throwTarget>throwWeight ? 14 : 6)*Time.Delta));
        if(enemy.SlipRemaining>0)
        {
            float age=OverdriveEnemy.SlipDuration-enemy.SlipRemaining;
            string sequence=age<1.35f ? "PB_fall" : "PB_getup";
            if(slipSource.Sequence.Name!=sequence)slipSource.Sequence.Name=sequence;
            slipSource.Sequence.Looping=false;slipSource.Sequence.TimeNormalized=age<1.35f ? (age/1.15f).Clamp(0,1) : ((age-1.35f)/1.3f).Clamp(0,1);
        }
        if(!enemy.Dead)currentMotion=enemy.SlipRemaining>0 ? slipSource : source;
        if(reactionSerial!=enemy.ReactionSerial)
        {
            reactionSerial=enemy.ReactionSerial;recoilTime=.3f;squashTime=.22f;
            hurtDuration=enemy.MissingParts!=previousParts ? .75f : .4f;hurtTime=hurtDuration;
        }
        previousParts=enemy.MissingParts;hurtTime=MathF.Max(0,hurtTime-Time.Delta);
        recoilTime=MathF.Max(0,recoilTime-Time.Delta);meleeFollowThrough=MathF.Max(0,meleeFollowThrough-Time.Delta);
        if(enemy.Dead || (enemy.MissingParts&1)!=0){BlinkWeight=0;blinkAge=-1;}
        else
        {
            blinkDelay-=Time.Delta;
            if(blinkAge<0 && blinkDelay<=0){blinkAge=0;blinkDelay=Game.Random.Float(2.6f,5.4f);}
            if(blinkAge>=0)
            {
                blinkAge+=Time.Delta;
                BlinkWeight=blinkAge<.075f ? (blinkAge/.075f).Clamp(0,1) : (1-(blinkAge-.075f)/.13f).Clamp(0,1);
                BlinkWeight=BlinkWeight*BlinkWeight*(3-2*BlinkWeight);
                if(blinkAge>=.205f){blinkAge=-1;BlinkWeight=0;}
            }
        }
        if(hitboxes.IsValid())hitboxes.Enabled=!enemy.Dead;
        var travel=WorldPosition-previous;previous=WorldPosition;
        var velocity=travel.Length>80 ? Vector3.Zero : travel/MathF.Max(.001f,Time.Delta);
        if(velocity.Length>260)velocity=velocity.Normal*260;
        smoothVelocity=Vector3.Lerp(smoothVelocity,velocity,1-MathF.Exp(-12*Time.Delta));
        // Short legs need a quicker cadence for the same navigation speed.
        float speed=smoothVelocity.WithZ(0).Length;
        float angerTarget=enemy.Dead ? 0 : enemy.Enraged || enemy.Windup>0 ? 1 : speed>25 ? .8f : .12f;
        anger+=(angerTarget-anger)*(1-MathF.Exp(-6*Time.Delta));
        source.PlaybackRate=1+(speed/205).Clamp(0,1)*.55f;
        animation.WithVelocity(smoothVelocity*1.6f);animation.WithWishVelocity(smoothVelocity*1.6f);
        // Glare at the nearest fighter: eyes and head follow, the torso barely.
        lookRefresh-=Time.Delta;
        if(lookRefresh<=0)
        {
            lookRefresh=Game.Random.Float(.25f,.4f);
            var nearest=Scene.GetAllComponents<PaintballCombatant>().Where(p=>p.AcceptHits)
                .OrderBy(p=>(p.WorldPosition-WorldPosition).LengthSquared).FirstOrDefault();
            hasLookPoint=nearest.IsValid() && (nearest.WorldPosition-WorldPosition).Length<1100;
            if(hasLookPoint)lookPoint=nearest.WorldPosition+Vector3.Up*58;
        }
        bool canLook=hasLookPoint && !enemy.Dead && enemy.SlipRemaining<=0 && (enemy.MissingParts&1)==0;
        var lookDirection=canLook ? (lookPoint-(WorldPosition+Vector3.Up*62)).Normal : WorldRotation.Forward;
        if(canLook && Vector3.Dot(lookDirection.WithZ(0).Normal,WorldRotation.Forward)<-.1f)lookDirection=WorldRotation.Forward;
        animation.WithLook(lookDirection,canLook ? 1 : 0,canLook ? .75f : 0,canLook ? .15f : 0);
        // Lean into the run and bank slightly into turns.
        float yaw=WorldRotation.Yaw();
        float yawRate=((yaw-previousYaw+540)%360-180)/MathF.Max(.001f,Time.Delta);previousYaw=yaw;
        float leanTarget=enemy.Dead ? 0 : (speed/205).Clamp(0,1)*5;
        float bankTarget=enemy.Dead || speed<25 ? 0 : (yawRate*.02f).Clamp(-5,5);
        lean+=(leanTarget-lean)*(1-MathF.Exp(-8*Time.Delta));
        bank+=(bankTarget-bank)*(1-MathF.Exp(-6*Time.Delta));
        squashTime=MathF.Max(0,squashTime-Time.Delta);idleClock+=Time.Delta;
        animation.DuckLevel=enemy.HollowTorso && !enemy.Dead ? .15f : 0;
        // Native HoldItem has a real backswing and release (about 0.4 seconds
        // after b_attack). Keep its follow-through after the prop leaves the hand.
        bool throwing=(enemy.RangedWindup || enemy.ThrowRecovery>0) && !enemy.Dead && enemy.SlipRemaining<=0;
        // Headbutt: wind back, then snap the head and torso forward into the
        // target, and settle. Smooth curves; the hit lands as the snap peaks.
        bool butting=enemy.Windup>0 && !enemy.RangedWindup && !enemy.Dead;
        if(butting){headbutt=1-enemy.Windup/OverdriveEnemy.HeadbuttTime;buttRecover=.32f;}
        else{headbutt=0;buttRecover=MathF.Max(0,buttRecover-Time.Delta);}
        if(butting && headbutt>=.86f && !buttSounded){buttSounded=true;KitchenCombatAudio.Play("bonk",enemy.AimPoint+WorldRotation.Forward*20);}
        if(!butting)buttSounded=false;
        if(throwing)source.PlaybackRate=1;
        animation.HoldType=throwing ? CitizenAnimationHelper.HoldTypes.HoldItem : CitizenAnimationHelper.HoldTypes.None;
        animation.Handedness=(enemy.MissingParts&4)==0 ? CitizenAnimationHelper.Hand.Right : CitizenAnimationHelper.Hand.Left;
        source.Set("holdtype_attack",0f);
        bool swing=enemy.RangedWindup && enemy.Windup>0 && enemy.Windup<=.4f && !enemy.Dead;
        if(swing && !attacking)source.Set("b_attack",true);
        attacking=swing;
        bool holding=Grabbed;
        if(holding && (!heldProp.IsValid() || heldPlate!=enemy.ThrowPlate || heldEgg!=enemy.ThrowEgg))
        {
            if(heldProp.IsValid())heldProp.Destroy();
            heldPlate=enemy.ThrowPlate;heldEgg=enemy.ThrowEgg;heldProp=SausageProjectile.CreateVisual(GameObject,heldPlate,heldEgg);
        }
        if(heldProp.IsValid())heldProp.Enabled=holding;
        if(enemy.Dead && !enemy.HeadlessStagger && !fallen){fallen=true;Body.GameObject.LocalScale=Vector3.One;OverdriveCorpse.Spawn(this,enemy.VisibleDamageParts,enemy.LastImpulse);}
    }
    private float headbutt,buttRecover;private bool buttSounded;
    // Torso pitch (degrees, + = forward) and forward lunge for the headbutt.
    private (float Pitch,float Lunge) HeadbuttPose()
    {
        static float Smooth(float x){x=x.Clamp(0,1);return x*x*(3-2*x);}
        if(headbutt>0)
        {
            if(headbutt<.62f)return (-20*Smooth(headbutt/.62f),-2*Smooth(headbutt/.62f));
            float k=Smooth((headbutt-.62f)/.3f);
            return (-20+54*k,-2+14*k);
        }
        float r=Smooth(buttRecover/.32f);
        return (34*r,12*r);
    }
    private float stackHeight,carryWeight,throwPhase,throwWeight;private bool throwFrisbee;
    private int shownKind,builtCount;
    private readonly List<GameObject> loadItems=new();
    // True from the moment the throwing hand takes an item out of the load.
    private bool Grabbed=>enemy.RangedWindup && enemy.Windup>0 && !enemy.Dead && enemy.SlipRemaining<=0 && (enemy.MissingParts&6)!=6
        && 1-enemy.Windup/OverdriveEnemy.ThrowWindup>=OverdriveEnemy.ThrowGrabAt;
    // Carton seats in the order the right hand empties them (nearest first).
    static readonly int[] seatOrder={0,3,1,4,2,5};
    static readonly Vector3[] seats={new(-5.5f,-2.65f,-2.75f),new(0,-2.65f,-2.75f),new(5.5f,-2.65f,-2.75f),new(-5.5f,2.65f,-2.75f),new(0,2.65f,-2.75f),new(5.5f,2.65f,-2.75f)};
    const float CartonScale=.86f;
    static bool loggedCarton;
    // Off-hand load: a stack of plates (plate throwers) or an open egg carton
    // (egg throwers). Built once; each item shows or hides as it is used, so
    // the load always shows exactly what is left (minus one in the hand).
    private void UpdateStack()
    {
        int kind=enemy.Dead || (enemy.MissingParts&6)==6 ? 0 : enemy.PlatesCarried>0 ? 1 : enemy.EggsCarried>0 ? 2 : 0;
        int carried=kind==1 ? enemy.PlatesCarried : kind==2 ? enemy.EggsCarried : 0;
        int visible=Math.Max(0,carried-(Grabbed && ((kind==1 && enemy.ThrowPlate) || (kind==2 && enemy.ThrowEgg)) ? 1 : 0));
        if(!OverdriveAssets.Ready)return;
        if(kind==0)
        {
            // Dying with a load drops it: smash it where it falls.
            if(shownPlates>0 && stack.IsValid())
            {
                if(shownKind==2){YolkBurst.Spawn(stack.WorldPosition);KitchenCombatAudio.Play("egg",stack.WorldPosition);}
                else{SausageCeramicBurst.Spawn(stack.WorldPosition);SausageKitchenAudio.Play(true,stack.WorldPosition);}
            }
            if(stack.IsValid())stack.Destroy();
            loadItems.Clear();shownPlates=0;shownKind=0;builtCount=0;StackCenter=null;return;
        }
        if(kind!=shownKind || !stack.IsValid() || carried>builtCount)Build(kind,carried);
        if(visible!=shownPlates)
        {
            shownPlates=visible;
            if(kind==1)for(int i=0;i<loadItems.Count;i++)loadItems[i].Enabled=i<visible;
            else for(int i=0;i<loadItems.Count;i++)loadItems[seatOrder[i]].Enabled=i>=6-visible;
        }
    }
    private void Build(int kind,int count)
    {
        if(stack.IsValid())stack.Destroy();
        loadItems.Clear();shownKind=kind;builtCount=count;shownPlates=-1;
        stack=new GameObject(GameObject,true,kind==1 ? "Carried plate stack" : "Carried egg carton"){NetworkMode=NetworkMode.Never};
        if(kind==1)
        {
            var model=Model.Load(OverdriveAssets.PlatePath);
            float scale=16/MathF.Max(.01f,MathF.Max(model.Bounds.Size.x,model.Bounds.Size.y));
            float step=MathF.Max(1.1f,model.Bounds.Size.z*scale*.6f);
            for(int i=0;i<count;i++)
            {
                var plate=SausageProjectile.CreateVisual(stack,true);
                plate.LocalScale=new Vector3(scale);plate.LocalPosition=Vector3.Up*(i*step+model.Bounds.Size.z*scale*.5f);
                plate.LocalRotation=Rotation.FromYaw(i*37);
                loadItems.Add(plate);
            }
            stackHeight=count*step;
        }
        else
        {
            // Open moulded-pulp carton: lid hinged toward the sausage so the
            // eggs face outward; six seats, emptied one by one.
            var carton=Model.Load(OverdriveAssets.EggCartonPath);
            var egg=Model.Load(OverdriveAssets.RealEggPath);
            // The carton is modelled Z-up with its lid hinged on one long side.
            // Work out from the imported bounds which horizontal axis the lid is
            // on (it shifts the centre that way) and turn the carton so the lid
            // stands up toward the sausage and the eggs face outward.
            var cb=carton.IsError ? new BBox(-Vector3.One,Vector3.One) : carton.Bounds;
            bool lidOnX=MathF.Abs(cb.Center.x)>MathF.Abs(cb.Center.y);
            var lidDir=lidOnX ? new Vector3(MathF.Sign(cb.Center.x),0,0) : new Vector3(0,MathF.Sign(cb.Center.y),0);
            float lidYaw=MathF.Atan2(lidDir.y,lidDir.x)*180/MathF.PI;
            var box=new GameObject(stack,true,"Egg carton"){NetworkMode=NetworkMode.Never};
            box.LocalRotation=Rotation.FromYaw(180-lidYaw);box.LocalScale=new Vector3(CartonScale);
            // Cup bottoms sit on the palm.
            box.LocalPosition=Vector3.Up*(-cb.Mins.z*CartonScale-.4f);
            if(!carton.IsError)box.Components.Create<ModelRenderer>().Model=carton;
            // Egg stands point-up in its cup whatever axis the file used.
            var es=egg.IsError ? Vector3.One : egg.Bounds.Size;
            var eggUp=es.z>=es.x && es.z>=es.y ? Rotation.Identity : es.x>=es.y ? Rotation.FromPitch(-90) : Rotation.FromRoll(90);
            float eggHalf=MathF.Max(es.z,MathF.Max(es.x,es.y))*.5f;
            for(int i=0;i<6;i++)
            {
                var e=new GameObject(box,true,"Carton egg"){NetworkMode=NetworkMode.Never};
                if(!egg.IsError)
                {
                    // Seats are laid out along the long axis (the one without the lid).
                    var seat=lidOnX ? new Vector3(seats[i].y,seats[i].x,seats[i].z) : seats[i];
                    e.LocalRotation=Rotation.FromYaw(i*60)*eggUp;
                    e.LocalPosition=seat+Vector3.Up*eggHalf-e.LocalRotation*egg.Bounds.Center;
                    e.Components.Create<ModelRenderer>().Model=egg;
                }
                loadItems.Add(e);
            }
            if(!loggedCarton){loggedCarton=true;Log.Info($"EGG_CARTON bounds={cb.Mins}..{cb.Maxs} lidOnX={lidOnX} lidYaw={lidYaw:0} eggSize={es}");}
            stackHeight=4;
        }
        Log.Info($"SAUSAGE_CARRY {(kind==1?"plates":"eggs")}={count} style={enemy.AttackStyle}");
    }
    internal static int Part(string name)
    {
        if(name is "head" or "neck_0" || name.StartsWith("face_") || name.StartsWith("eye_") || name.StartsWith("ear_"))return 1;
        bool left=name.Contains("_L");
        if(name.StartsWith("arm_") || name.StartsWith("hand_") || name.StartsWith("finger_") || name.StartsWith("hold_"))return left ? 2 : 4;
        if(name.StartsWith("leg_") || name.StartsWith("ankle_") || name.StartsWith("ball_") || name.StartsWith("foot_"))return left ? 8 : 16;
        return 0;
    }
    internal static string CutAnchor(int part)=>part switch {1=>"neck_0",2=>"clavicle_L",4=>"clavicle_R",_=>"pelvis"};
    internal static void ApplyParts(SkinnedModelRenderer renderer,int missing,int only=-1,bool hollow=false,int headVariant=2)
    {
        foreach(var item in new[]{("Torso",0),("Head",1),("LeftArm",2),("RightArm",4),("LeftLeg",8),("RightLeg",16)})
            renderer.SetBodyGroup(item.Item1,(only>=0 ? item.Item2!=only : (missing&item.Item2)!=0) ? 1 : 0);
        if(only<0 && (missing&1)!=0)renderer.SetBodyGroup("Head",headVariant.Clamp(2,4));
        // A partial head retains the neck: do not also remove the torso's neck
        // seam, which created a gap underneath the surviving half.
        if(only<0)renderer.SetBodyGroup("Torso",hollow ? 2 : 0);
        renderer.SetBodyGroup("Socket_Torso_Head",1);
        renderer.SetBodyGroup("Socket_Head_Torso",only==1 ? 0 : 1);
        foreach(var limb in new[]{("LeftArm",2),("RightArm",4),("LeftLeg",8),("RightLeg",16)})
        {
            renderer.SetBodyGroup("Socket_Torso_"+limb.Item1,only<0 && (missing&limb.Item2)!=0 ? 0 : 1);
            renderer.SetBodyGroup("Socket_"+limb.Item1+"_Torso",only==limb.Item2 ? 0 : 1);
        }
    }
    protected override void OnPreRender()
    {
        if(!GameObject.IsValid() || !GameObject.Active || !enemy.IsValid()
            || !Body.IsValid() || !Body.Enabled || !Body.SceneObject.IsValid()
            || !source.IsValid() || fallen)return;
        if(enemy.IceShattered){Body.Enabled=false;return;}
        Body.MaterialOverride=enemy.FrozenRemaining>0 ? CryoBurst.Frost : null;
        
        if(enemy.FrozenRemaining>0 && poses.Count>0)
        {
            foreach(var bone in Body.Model.Bones.AllBones)Body.SetBoneTransform(bone,poses.TryGetValue(bone.Name,out var frozenPose) ? frozenPose : Body.Model.GetBoneTransform(bone.Name));
            Body.Tint=Color.White;
            return;
        }
        float slipBlend=enemy.SlipRemaining>0 ? MathF.Min((OverdriveEnemy.SlipDuration-enemy.SlipRemaining)/.16f,enemy.SlipRemaining/.18f).Clamp(0,1) : 0;
        float kick=MathF.Sin((.3f-recoilTime)*MathF.PI/.3f)*(recoilTime/.3f);
        var direction=WorldRotation.Inverse*enemy.LastImpulse;
        float hurt=enemy.Dead ? 0 : MathF.Sin((1-hurtTime/hurtDuration)*MathF.PI);
        float limp=enemy.Dead ? 0 : (enemy.MissingParts&24) switch {8=>-5f,16=>5f,_=>0f};
        float gait=slipBlend>0 ? 0 : 1;
        var butt=HeadbuttPose();
        retarget.Update(source,slipSource,slipBlend,new Angles(-direction.x*9*kick+hurt*12+lean*gait+butt.Pitch,0,direction.y*7*kick+limp+bank*gait),enemy.MissingParts);
        // Load carriers hold the off arm bent in front, like a waiter.
        string carrySide=(enemy.MissingParts&2)==0 ? "L" : "R";
        // Throwing arm, driven by the wind-up and recovery clocks.
        string throwSide=(enemy.MissingParts&4)==0 ? "R" : "L";
        retarget.Carry(carrySide,carryWeight*(throwSide==carrySide ? 1-throwWeight : 1));
        retarget.Throw(throwSide,MathF.Min(throwPhase,1.99f),throwFrisbee,throwWeight);
        // Cartoon squash on impact, and a slow idle breath while standing.
        float squash=squashTime>0 ? MathF.Sin((squashTime/.22f)*MathF.PI)*.04f : 0;
        float breath=enemy.Dead || smoothVelocity.WithZ(0).Length>25 ? 0 : MathF.Sin(idleClock*2.3f+GameObject.Id.GetHashCode()%7)*.012f;
        Body.GameObject.LocalScale=new Vector3(1+squash*.55f,1+squash*.55f,1-squash+breath);
        Body.GameObject.LocalPosition=Vector3.Forward*butt.Lunge;
        poses.Clear();
        foreach(var p in retarget.Poses)poses[p.Key]=p.Value;
        foreach(var bone in Body.Model.Bones.AllBones)
        {
            var pose=poses.TryGetValue(bone.Name,out var fittedPose) ? fittedPose : Body.Model.GetBoneTransform(bone.Name);
            Body.SetBoneTransform(bone,pose);
        }
        if(appliedParts!=enemy.VisibleDamageParts || appliedHollow!=enemy.HollowTorso || appliedHead!=enemy.HeadBreakVariant)
        {
            ApplyParts(Body,enemy.VisibleDamageParts,hollow:enemy.HollowTorso,headVariant:enemy.HeadBreakVariant);
            appliedParts=enemy.VisibleDamageParts;appliedHollow=enemy.HollowTorso;appliedHead=enemy.HeadBreakVariant;
        }
        bool faceVisible=(enemy.MissingParts&1)==0;
        SetFaceMorph("Blink",faceVisible ? MathF.Max(BlinkWeight,hurt*.65f) : 0);
        SetFaceMorph("Angry",faceVisible ? anger*(1-hurt) : 0);
        SetFaceMorph("Wince",faceVisible ? hurt : 0);
        // A warm flush with a slow, restrained pulse; keep the casing readable.
        float flush=rageFlush*(.8f+.08f*MathF.Sin(idleClock*2));
        Body.Tint=Color.Lerp(Color.White,new Color(1,.82f,.77f),flush);
        string offSide=(enemy.MissingParts&2)==0 ? "L" : "R";
        if(stack.IsValid())
        {
            // Use this frame's fitted pose, not last frame's world bone cache.
            stack.WorldPosition=Body.WorldTransform.PointToWorld(retarget.PalmCenter(offSide));
            stack.WorldRotation=Rotation.FromYaw(WorldRotation.Yaw());
            StackCenter=stack.WorldPosition+Vector3.Up*stackHeight*.5f;
        }
        if(heldProp.IsValid() && heldProp.Enabled)
        {
            var frame=Body.WorldRotation*retarget.PalmFrame(throwSide);
            heldProp.WorldPosition=Body.WorldTransform.PointToWorld(retarget.PalmCenter(throwSide))+frame.Up*(heldEgg ? 1.3f : 0);
            heldProp.WorldRotation=frame;
        }
    }

    private void SetFaceMorph(string name,float weight)
    {
        // Resting faces need no override. Do not resubmit identical weights for
        // every camera, or queue a new blend while a renderer is tearing down.
        weight=float.IsFinite(weight) ? weight.Clamp(0,1) : 0;
        if(weight<.001f)
        {
            if(Body.Morphs.ContainsOverride(name))Body.Morphs.Clear(name,0);
            return;
        }
        if(!Body.Morphs.ContainsOverride(name) || MathF.Abs(Body.Morphs.Get(name)-weight)>.001f)
            Body.Morphs.Set(name,weight,0);
    }
}