PhysicalInterceptions.cs

Part of the MatchSimulation class for a football match. Decides which outfield players attempt to intercept a moving ball, tests the ball's path against the player's limb rig for a body contact, and reflects the ball as a rebound/deflection. Tracks interception attempts and physical deflections.

Rough Code
using System;
using System.Collections.Generic;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
    readonly List<ReplayFigure.Part> contactPose=new();
    public int PhysicalDeflections,InterceptionAttempts;
    void PrepareInterceptions()
    {
        if(Owner>=0 || BallSpeed<3 || RehearsingStrategy)return;
        foreach(var p in ActivePlayers.Where(p=>!p.Keeper && !p.IsAcrobaticCommitted(Seconds) && !PassiveTrainingOpponent(p) && p.Team!=LastTouch && Seconds>=p.FallUntil && Seconds-p.LastSlideAt>=2.1f))
        {
            if(Seconds<p.InterceptUntil+.12f)continue;
            PrepareLegendaryInterception(p);
            float time=Point.Dot(p.Position-Ball,BallVelocity)/Math.Max(1,Point.Dot(BallVelocity,BallVelocity));
            float reading=Math.Clamp(p.Profile.Interceptions,0,1);
            float anticipation=.29f+reading*.17f+.09f*SkillStrength(p,UniqueSkill.Interceptor)+(LegendaryActive(p,LegendaryAbility.EntryDenied)?.14f:0);
            if(time<.04f || time>anticipation)continue;
            var meeting=Ball+BallVelocity*time;
            float height=BallHeight+VerticalSpeed*time-4.905f*time*time;
            float normalReach=.59f+reading*.36f;
            float expanded=normalReach+.20f*SkillStrength(p,UniqueSkill.Interceptor);
            if(height>1.5f || (meeting-p.Position).Length>expanded)continue;
            var forward=new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing));
            if(Point.Dot((Ball-p.Position).Unit,forward)<-.35f && Seconds-p.LastScanAt>.35f)continue;
            bool interceptor=SkillActive(p,UniqueSkill.Interceptor) || ActivateSkill(p,UniqueSkill.Interceptor,.65f);
            if((meeting-p.Position).Length>normalReach && !interceptor)continue;
            var reach=meeting-p.Position;
            float footReach=.6f+(interceptor?.18f*p.ActiveSkillPower:0);
            if(reach.Length>footReach)reach=reach.Unit*footReach;
            p.InterceptAim=p.Position+reach;p.InterceptHeight=Math.Clamp(height,.08f,.48f);
            p.InterceptBody=height>.65f;
            p.InterceptAt=Seconds;p.InterceptUntil=Seconds+.5f;
            p.InterceptFoot=Point.Dot(reach,new Point(-forward.Y,forward.X))<0?-1:1;
            p.DecisionReason=p.InterceptBody?"Перекрывает передачу корпусом":"Видит передачу · выставляет ногу";InterceptionAttempts++;
        }
    }
    bool TryBodyContact(Footballer p,Point before,float beforeHeight,out Point contact,out float height,out Point normal)
    {
        contact=Ball;height=BallHeight;normal=new();
        if(p.IsAcrobaticCommitted(Seconds))return false;
        if(DistanceToSegment(p.Position,before,Ball)>1.25f || Math.Min(beforeHeight,BallHeight)>1.7f)return false;
        ReplayFigure.Pose(p,this,contactPose);
        var path=Ball-before;
        int steps=Math.Max(1,(int)MathF.Ceiling(MathF.Sqrt(path.Length*path.Length+(BallHeight-beforeHeight)*(BallHeight-beforeHeight))/.045f));
        for(int step=0;step<=steps;step++)
        {
            float fraction=(float)step/steps;var at=before+path*fraction;
            float z=Math.Max(.11f,beforeHeight+(BallHeight-beforeHeight)*fraction);
            // Same bounded rig as live rendering. Legs, boots and torso, not a player-radius trigger.
            for(int i=0;i<=6;i++)
            {
                var part=contactPose[i];var axis=part.B-part.A;float dz=part.ZB-part.ZA;
                float t=Math.Clamp((Point.Dot(at-part.A,axis)+(z-part.ZA)*dz)/Math.Max(.00001f,Point.Dot(axis,axis)+dz*dz),0,1);
                var nearest=part.A+axis*t;float nearestZ=part.ZA+dz*t;
                float radius=.11f+part.Width*.5f;
                if((at-nearest).Length*(at-nearest).Length+(z-nearestZ)*(z-nearestZ)>radius*radius)continue;
                contact=at;height=Math.Max(0,z);normal=(at-nearest).Unit;
                if(normal.Length<.1f)normal=BallVelocity.Unit*-1;
                return true;
            }
        }
        return false;
    }
    void ReboundInterception(Footballer p,Point normal)
    {
        float speed=BallSpeed;
        if(speed<3.5f+(SkillActive(p,UniqueSkill.Interceptor)?2.4f*p.ActiveSkillPower:0) && BallHeight<.35f && Seconds<p.InterceptUntil && p.Profile.Interceptions>.65f)
        {TakeBall(p);return;}
        SnapshotOffside(p,false,true);
        RegisterShotContact("РИКОШЕТ");if(shot)ShotBlocks++;
        var reflected=BallVelocity-normal*(2*Math.Min(0,Point.Dot(BallVelocity,normal)));
        BallVelocity=reflected*(.42f+p.Profile.Defence*.10f)+p.Velocity*.12f;
        VerticalSpeed=BallHeight>.45f?-.7f:.45f+Math.Min(1.5f,speed*.035f);
        BallSpin*=.25f;Owner=-1;Passer=-1;LastTouch=p.Team;shot=false;flightAge=0;PickupDelay=.10f;
        p.RecoveryChaseUntil=Seconds+1.8f;p.Action="Перекрыл · рикошет";
        PhysicalDeflections++;Emit("block",p,"РИКОШЕТ",1);
    }
}