FootballAcrobatics.cs

Part of the soccer match simulation. Handles the landing recovery after an acrobatic shot: captures the footballer's residual velocity at take-off, integrates it over a short decay curve to slide the position forward, and steps that recovery both in normal play and while match time is frozen for whistles or treatment.

Rough Code
using System;

namespace Socer;

public sealed partial class MatchSimulation
{
    const float AcrobaticFlightSeconds=.28f;
    const float AcrobaticMaximumInertia=2.2f;

    void BeginAcrobaticRecovery(Footballer p)
    {
        float duration=LegendaryFootballAnimation.Get(LegendaryAbility.RoyalReception).Duration;
        p.AcrobaticAt=Seconds;p.AcrobaticUntil=Seconds+duration;
        p.AcrobaticVelocity=p.Velocity.Length>AcrobaticMaximumInertia?
            p.Velocity.Unit*AcrobaticMaximumInertia:p.Velocity;
        p.Velocity=new Point();
        p.RunUntil=p.FootballRunUntil=p.LegendaryRunUntil=0;p.BurstRemaining=0;
        p.PlanUntil=p.KickSetupUntil=p.ShotSetupUntil=0;p.ChasingTouch=false;
        p.ReceiveReadyUntil=p.ReceiveUntil=p.ChestUntil=Seconds;
        p.FeintUntil=p.InterceptUntil=p.BlockUntil=Seconds;
        p.PassCallUntil=0;p.Action="Приземляется после акробатического удара";
    }

    void AdvanceAcrobaticRecovery(Footballer p,float dt)
    {
        if(p.AcrobaticUntil<=p.AcrobaticAt || Seconds<p.AcrobaticAt)return;
        float age=Seconds-p.AcrobaticAt;
        // Exact integration of a short, linearly decaying launch velocity.
        // The total displacement is <=2.2*.28/2=.308 m, then both roots plant.
        float Integral(float t)
        {t=Math.Clamp(t,0,AcrobaticFlightSeconds);return t-t*t/(2*AcrobaticFlightSeconds);}
        float distance=Integral(age)-Integral(age-dt);
        if(distance>0)p.Position=Clamp(p.Position+p.AcrobaticVelocity*distance);
        if(p.IsAcrobaticCommitted(Seconds))
        {
            p.Velocity=new Point();p.BurstRemaining=0;
            p.Action=age<.48f?"Приземляется после акробатического удара":"Поднимается после акробатического удара";
        }
    }
    void AdvanceAcrobaticRecoveries(float dt)
    {
        foreach(var p in Players)AdvanceAcrobaticRecovery(p,dt);
    }

    void AdvanceFrozenAcrobaticRecoveries(float dt)
    {
        foreach(var p in Players)
        {
            if(!p.IsAcrobaticCommitted(Seconds))continue;
            // Whistles and treatment stop match time, but an already airborne
            // body must finish landing before any presentation walk can start.
            p.AcrobaticAt-=dt;p.AcrobaticUntil-=dt;
            AdvanceAcrobaticRecovery(p,dt);
        }
    }
}