LooseBallPlay.cs

Part of the soccer match simulation. Predicts where a ball will be reachable for aerials and loose balls, assigns chase and header actions, and decides whether a keeper can claim a loose ball, including AerialGuardian and SweeperKeeper skill use.

using System;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
    public int KeeperClaims;
    public int AnticipatedJumps;
    Point AerialMeeting()
    {
        var at=Ball;var velocity=BallVelocity;float height=BallHeight,vertical=VerticalSpeed,spin=BallSpin;
        const float dt=.06f;
        for(float t=0;t<4;t+=dt)
        {
            float angle=spin*dt,c=MathF.Cos(angle),s=MathF.Sin(angle);
            velocity=new Point(velocity.X*c-velocity.Y*s,velocity.X*s+velocity.Y*c);spin*=MathF.Exp(-dt*.4f);
            at+=velocity*dt;height+=vertical*dt-4.905f*dt*dt;vertical-=9.81f*dt;velocity*=MathF.Exp(-dt*.08f);
            if(height<=1.85f && vertical<0)break;
        }
        return Clamp(at);
    }
    Point AerialMovement(Footballer p,Point target)
    {
        p.AerialChasing=false;
        if(RehearsingStrategy || Owner>=0 || ShotInFlight || p.Keeper || (BallHeight<.85f && VerticalSpeed<=2))return target;
        var meeting=AerialMeeting();
        var contest=ActivePlayers.Where(q=>q.Team==p.Team && !q.Keeper && Seconds>=q.FallUntil)
            .OrderBy(q=>(q.Position-meeting).Length/Math.Max(1,EffectivePace(q))-(q.Id==PassReceiver?.3f:0)).Take(2).ToArray();
        int slot=Array.FindIndex(contest,q=>q.Id==p.Id);
        if(slot<0 || (meeting-p.Position).Length>35)return target;
        p.AerialChasing=true;p.Action=slot==0?"Выходит под навес":"Страхует верховую борьбу";
        if(slot==0 && (meeting-p.Position).Length<1.25f && VerticalSpeed<0 && BallHeight is >1.7f and <3.2f &&
            Seconds-p.AerialContestAt>.42f)
        {p.AerialContestAt=Seconds;p.HeaderLeap=Math.Clamp((p.Profile.Jumping-.35f)*.34f,.08f,.22f);AnticipatedJumps++;}
        // The delivery and reachable ball height decide whether to contest a
        // header. Technique controls the contest and accuracy, not permission.
        bool plannedHeader=p.Id==PassReceiver && p.Profile.HeadingAccuracy>.35f && p.Profile.Jumping>.3f &&
            World(meeting,p.Team).X>83 && Math.Abs(World(meeting,p.Team).Y-34)<17 &&
            Passer>=0 && Math.Abs(World(Players[Passer].Position,p.Team).Y-34)>12;
        return slot==0?(p.Id==PassReceiver && !plannedHeader?ReceptionMeeting(p):meeting):
            Clamp(meeting+new Point(-Direction(p.Team)*2.3f,p.Position.Y<meeting.Y?-2:2));
    }
    Point LooseBallMeeting(Footballer p)
    {
        float pace=Math.Max(1,p.Pace*(.64f+.36f*p.Stamina)*(p.BurstCharge>.2f?1.48f:1));
        Point target=Ball;
        for(float t=.1f;t<=1.6f;t+=.1f)
        {
            float drag=BallHeight>.05f?.08f:.28f;
            target=Clamp(Ball+BallVelocity*((1-MathF.Exp(-drag*t))/drag));
            float travel=(target-p.Position).Length;
            float accelerationDelay=Math.Max(0,pace-p.Velocity.Length)*.035f;
            if(travel/pace+accelerationDelay<=t)return target;
        }
        return target;
    }
    bool UrgentLooseBall(Footballer p)=>Owner<0 && RestartRemaining<=0 && GoalHoldRemaining<=0 && !p.Keeper &&
        ((Math.Min(Ball.X,105-Ball.X)<28 && Math.Abs(Ball.Y-34)<25 && p.Id==chasers[p.Team] && (p.Position-Ball).Length<28) ||
        (!RehearsingStrategy && (p.Id==PassReceiver || Seconds<p.RecoveryChaseUntil) && p.Team==LastTouch && (p.Position-Ball).Length<18));
    Point LooseBallTarget(Footballer p,Point fallback)
    {
        if(Owner>=0 || p.Keeper || p.Id!=chasers[p.Team])return fallback;
        p.RunUntil=0;p.Action=World(Ball,p.Team).X>70?"На добивание":"К свободному мячу";
        return LooseBallMeeting(p);
    }
    bool TryKeeperClaim(Footballer p,out Point target)
    {
        target=p.Position;
        if(Owner>=0 || RestartRemaining>0 || GoalHoldRemaining>0 || p.DiveRemaining>0 || p.Recovery>.15f || (ShotInFlight && BallSpeed>12))return false;
        var local=World(Ball,p.Team);
        if(local.X<.3f || local.X>18 || Math.Abs(local.Y-34)>21)return false;
        target=LooseBallMeeting(p);var meeting=World(target,p.Team);
        if(meeting.X<.3f || meeting.X>16 || Math.Abs(meeting.Y-34)>19.5f)return false;
        float distance=(target-p.Position).Length;
        if(distance>13)return false;
        float pace=Math.Max(1,p.Pace*(.64f+.36f*p.Stamina)*(p.BurstCharge>.2f?1.48f:1));
        float arrival=distance/pace+.12f;
        float height=BallHeight+VerticalSpeed*arrival-4.905f*arrival*arrival;
        if(height>2.1f)return false;
        var intercept=target;
        float rival=ActivePlayers.Where(q=>q.Team!=p.Team).Min(q=>(q.Position-intercept).Length/Math.Max(1,q.Pace*(.64f+.36f*q.Stamina)*1.35f));
        bool close=(Ball-p.Position).Length<1.7f;
        bool skillCouldClaim=arrival<rival+.36f*SkillStrength(p,UniqueSkill.AerialGuardian) || close;
        // The committed aerial ability lasts for its active window. Readiness
        // becomes false immediately after paying its cost/starting cooldown;
        // it must not cancel the enhanced claim on the following frame.
        if(SkillActive(p,UniqueSkill.AerialGuardian) &&
            (arrival<rival+.36f*p.ActiveSkillPower || close))return true;
        if(skillCouldClaim && BallHeight>.45f && (height>1.25f || arrival>=rival-.04f) &&
            Seconds>=p.UniqueSkillConsiderAt && SkillReady(p,UniqueSkill.AerialGuardian))
        {
            p.UniqueSkillConsiderAt=Seconds+22;
            if(Next()<.5f && ActivateSkill(p,UniqueSkill.AerialGuardian))return true;
        }
        if(height<1.2f && (arrival<rival+.23f*SkillStrength(p,UniqueSkill.SweeperKeeper) || close) && !ShotInFlight &&
            (SkillActive(p,UniqueSkill.SweeperKeeper) || ActivateSkill(p,UniqueSkill.SweeperKeeper)))
            return arrival<rival+.23f*p.ActiveSkillPower || close;
        // Commit when first to the meeting point, or when the loose ball is already within reach.
        return height<=1.5f && (arrival<rival+.12f || close);
    }
}