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