FallPose.cs

Part of the match simulation and human figure drawing for a football game. Handles challenge impacts (slide tackles and shoulder contact), triggers falls with variant selection, advances fall and impact motion per tick, and builds the fallen player's body parts for rendering.

Rough Code
using System;
using System.Collections.Generic;
namespace Socer;
public sealed partial class MatchSimulation
{
    void RegisterChallengeImpact(Footballer offender,Footballer victim,bool slide)
    {
        var direction=(victim.Position-offender.Position).Unit;
        offender.ImpactAt=Seconds;offender.ImpactUntil=Seconds+(slide?.85f:.48f);
        offender.ImpactSlide=slide;offender.ImpactDirection=direction;
        offender.ImpactPoint=victim.Position-direction*.14f;
        offender.ImpactVelocity=slide?direction*Math.Clamp(offender.Velocity.Length,2.2f,5):direction*-.6f;
        if(slide){offender.SlideDirection=direction;offender.LastSlideAt=Seconds-.12f;}
        else offender.StumbleUntil=Seconds+.48f;
    }
    void FallDown(Footballer p,Point direction,float severity)
    {
        var facing=new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing));
        float impact=Point.Dot(direction.Unit,facing);
        p.FallSpeed=p.Velocity.Length;p.FallSeverity=Math.Clamp(severity,0,1);
        p.FallVariant=p.FallSpeed>4.8f && severity>.65f?4:impact<-.45f?1:impact>.45f?0:direction.Y*facing.X-direction.X*facing.Y>0?2:3;
        p.FallAt=Seconds+.055f;p.FallUntil=Seconds+2+p.FallSeverity*.65f;
        p.FallDirection=direction.Length>.01f?direction.Unit:new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing));
        p.FallVelocity=p.FallDirection*(2.8f+p.FallSeverity*2.8f)+p.Velocity*.45f;
        p.Velocity=new();p.VisualSpeed=0;p.BurstRemaining=0;p.RunUntil=0;p.Action="Падает";
        p.Recovery=Math.Max(p.Recovery,p.FallUntil-Seconds);
        AssessFallInjury(p,severity);
    }
    void AdvanceFalls(float dt)
    {
        foreach(var p in Players)
        {
            if(p.SentOff && Seconds>=p.ImpactUntil)continue;
            if(Seconds<p.ImpactUntil && Seconds>=p.FallUntil)
            {
                p.Position=Clamp(p.Position+p.ImpactVelocity*dt);
                p.ImpactVelocity*=MathF.Exp(-dt*(p.ImpactSlide?3:7));
                p.Velocity=new();p.Action=p.ImpactSlide?"Завершает подкат":"Теряет равновесие после контакта";
            }
            float age=InjuryAnimationSeconds(p)-p.FallAt;
            if(age<0 || age>.62f || p.FallVelocity.Length<.02f)continue;
            p.VisualBefore=p.Position;
            p.Position=Clamp(p.Position+p.FallVelocity*dt);
            p.VisualVelocity=(p.Position-p.VisualBefore)*(1/Math.Max(dt,.001f));
            p.FallVelocity*=MathF.Exp(-dt*4.2f);
            p.Action="Падает от столкновения";
        }
    }
}
public static partial class HumanFigure
{
    public static float FallBlend(Footballer p,MatchSimulation m)
    {
        float animationSeconds=m.InjuryAnimationSeconds(p);
        float age=animationSeconds-p.FallAt,remaining=p.FallUntil-animationSeconds;
        return Smooth(age/(p.FallVariant==4?.58f:.44f))*Smooth(remaining/.75f);
    }
    static List<Part> FallenPose(Footballer p,MatchSimulation m,List<Part> buffer=null)
    {
        var parts=buffer??new List<Part>(16);float fall=FallBlend(p,m),angle=fall*1.5f;
        var axis=p.FallDirection;var side=new Point(-axis.Y,axis.X);
        Point Joint(float lateral,float ahead,float z)
        {
            var ground=side*lateral+axis*(ahead*MathF.Cos(angle)+(z-.5f)*MathF.Sin(angle));
            float height=.04f+z*MathF.Cos(angle)-ahead*MathF.Sin(angle);
            return new(ground.X,ground.Y*GroundScale-height*.77f);
        }
        string shirt=p.Keeper?"#f5dc68":p.Team==0?"#46c7ed":"#fc8b61";
        string shorts=p.Team==0?"#193d60":"#f3e4cb";
        string skin=PlayerHeadProfiles.Skin(p.Profile.CatalogId,p.Profile.SkinTone,p.Id);
        void Bone(Point a,Point b,float w,string color)=>parts.Add(new(a,b,w,color));
        foreach(int s in new[]{-1,1})
        {
            var hip=Joint(s*.14f,0,.85f);var knee=Joint(s*.18f,-fall*.22f,.46f);var foot=Joint(s*.2f,-fall*.13f,.07f);
            Bone(hip,knee,.21f,shorts);Bone(knee,foot,.14f,shirt);Bone(foot,foot+axis*.15f,.16f,"#17282b");
        }
        Bone(Joint(0,0,.85f),Joint(0,0,1.38f),.48f,shirt);
        foreach(int s in new[]{-1,1})
        {
            var shoulder=Joint(s*.26f,0,1.33f);var elbow=Joint(s*.4f,-fall*.12f,1.05f);var hand=Joint(s*.34f,-fall*.08f,.8f);
            Bone(shoulder,elbow,.17f,shirt);Bone(elbow,hand,.12f,skin);
        }
        Bone(Joint(0,0,1.47f),Joint(0,0,1.7f),.31f,skin);Bone(Joint(0,0,1.7f),Joint(0,0,1.77f),.29f,PlayerHeadProfiles.Hair(p.Profile.CatalogId,p.Profile.HairTone));return parts;
    }
}