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);
}
}