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