Part of the football match simulation that drives dribbling. Checks ball-at-foot distance, steers the carrier toward a target, handles chest control, first-time contacts and rival tackles, then computes reach, cadence and impulse for each new dribble touch.
using System;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
public const float DribbleFootReach=.55f;
public int DribbleTouches, DribbleRecoveries, ExposedBallWins;
// Owner is the player attempting to control possession, not a constraint attaching the ball.
public bool BallAtFoot(Footballer p)=>(Ball-p.Position).Length<=.88f && BallHeight<.45f;
Point DribbleMovement(Footballer p,Point target)
{
if(Owner!=p.Id || p.Keeper || WindupPlayer==p.Id)return target;
float gap=(Ball-p.Position).Length;
if(gap>.82f)p.ChasingTouch=true;
else if(gap<.46f)p.ChasingTouch=false;
if((target-p.Position).Length>.2f)p.CarryLook=(target-p.Position).Unit;
if(p.ChasingTouch)
{
p.Action="Догоняет касание";
var route=p.CarryLook.Length>.1f?p.CarryLook:new Point(Direction(p.Team),0);
// Run through a ball still ahead of the boots. Braking at the ball's
// current point makes a sprint collapse just before the next touch.
return Clamp(Ball+BallVelocity*.08f+
(Point.Dot(Ball-p.Position,route)>0?route*2.2f:new Point()));
}
return target;
}
void AdvanceDribble(float dt)
{
var p=Players[Owner];
if(p.ChestUntil>Seconds)
{
float duration=p.ReceiveOnMove?.22f:.32f+(1-p.Skill)*.18f;
BallHeight=p.ChestHeight*Math.Clamp((p.ChestUntil-Seconds)/duration,0,1);
BallVelocity*=MathF.Exp(-dt*(p.ReceiveOnMove?2.4f:8));Ball+=BallVelocity*dt;return;
}
if(WindupPlayer==p.Id)BallVelocity*=MathF.Exp(-dt*14);
var next=Ball+BallVelocity*dt;
if(next.X<0 || next.X>105 || next.Y<0 || next.Y>68)
{CancelWindup();Owner=-1;Passer=-1;shot=false;MoveBall(dt);return;}
var before=Ball;
Ball+=BallVelocity*dt;
BallVelocity*=MathF.Exp(-dt*2.4f);
if(AdvanceSkillAir(p))return;
BallHeight=0;
float gap=(Ball-p.Position).Length;
// A shot added at a paused receiving checkpoint must precede any new dribble touch.
if(FirstTimePhase(p)>=0 && gap<=DribbleFootReach)
{firstTimeContact=p.Id;firstTimeIncomingSpeed=BallSpeed;ReleaseStrategyFirstTime();return;}
// No guaranteed possession between touches: the opponent can physically reach the ball first.
if(!StrategyWalkthrough && !StrategyDummyMode && gap>.82f && WindupPlayer!=p.Id)
{
var rival=ActivePlayers.Where(q=>q.Team!=p.Team && !q.Keeper && q.Recovery<=0 && Seconds>=q.FallUntil &&
!(p.FeintVariant==4 && p.SkillMove==SkillMoveKind.Nutmeg && p.SkillStage==1 && q.Id==p.SkillOpponent))
.OrderBy(q=>(q.Position-Ball).Length).FirstOrDefault();
if(rival!=null && DistanceToSegment(rival.Position,before,Ball)<.48f && (rival.Position-Ball).Length+.3f<gap)
{ExposedBallWins++;Tackles[rival.Team]++;CancelWindup();TakeBall(rival);return;}
}
if(gap>2.8f)
{CancelWindup();Owner=-1;Passer=-1;shot=false;flightAge=0;DribbleRecoveries++;p.Action="Отпустил мяч";return;}
if(AdvanceScissorsTouch(p,gap) || AdvanceSkillMoveTouch(p,gap))return;
// Decide at a real contact BEFORE pushing the ball away for another stride.
// Preparation remains committed and is advanced independently of thinking cadence.
if(!IsMediaLeague && !RehearsingStrategy && gap<=DribbleFootReach && WindupPlayer<0 && Seconds>=p.ReceiveUntil &&
Seconds>=p.ChestUntil && Seconds>=p.FeintUntil && Seconds>=p.FallUntil && p.Held>.2f)
{
float distance=(World(new Point(105,34),p.Team)-p.Position).Length;
if(p.ShotSetupUntil>0)
{if(SetUpShot(p,distance,CarrierPressure(p)))return;}
else if(distance<40 && p.Decision<=0 && (p.LastBallDecision=SelectActionPlan(p,CarrierPressure(p))).Choice==BallChoice.Shoot)
{
p.DecisionReason="Завершает выбранный удар на ближайшем касании";
if(SetUpShot(p,distance,CarrierPressure(p)))return;
}
}
// Cadence is a preferred rhythm, not a lockout that lets the runner overtake
// a reachable ball. Take a shorter adjustment step before it leaves foot reach.
var relative=Ball-p.Position;
var relativeVelocity=BallVelocity-p.Velocity;
bool adjustment=p.Velocity.Length>1 && Seconds-p.LastDribbleTouch>=.12f &&
Point.Dot(relativeVelocity,p.Velocity.Unit)<-.5f &&
Point.Dot(relative+relativeVelocity*.14f,p.Velocity.Unit)<.24f;
if(WindupPlayer==p.Id || (Seconds<p.NextDribbleTouch && !adjustment) || gap>DribbleFootReach)return;
float speed=p.Velocity.Length;
var direction=speed>.3f?p.Velocity.Unit:new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing));
if(p.CarryLook.Length>.1f)direction=(direction*.25f+p.CarryLook*.75f).Unit;
if(Seconds<p.ReceiveUntil)direction=p.ReceiveDirection;
if(Seconds<p.FeintUntil)direction=p.FeintDirection;
if(Seconds<p.ShieldUntil)direction=p.ShieldDirection;
if(!RehearsingStrategy && p.CarryAvoidUntil>Seconds && Seconds>=p.ShieldUntil)
{
var avoid=(p.CarryAvoidTarget-p.Position).Unit;
if(avoid.Length>.1f)direction=avoid;
else speed=0;
}
if(direction.Length<.1f)direction=new Point(Direction(p.Team),0);
float quality=Math.Clamp(p.Skill*.7f+p.Stamina*.3f,0,1);
float pressure=CarrierPressure(p);
float lane=CarryLane(p,p.Position+direction*3);
bool turning=speed>1.2f && Point.Dot(direction,p.Velocity.Unit)<.72f;
bool closeControl=pressure<5 || lane<1.7f || turning || Seconds<p.ShieldUntil || Seconds<p.FeintUntil;
float reach=speed<.6f?.28f:Math.Clamp(.5f+speed*.12f+(p.Sprinting?.15f:0)+(1-quality)*.12f,.5f,1.35f);
if(closeControl)reach=Math.Min(.48f,reach*.45f);
float pressureControl=closeControl?SkillStrength(p,UniqueSkill.PressResistant):0;
reach*=1-.12f*pressureControl;
bool gravity=LegendaryActive(p,LegendaryAbility.OtherGravity);
if(gravity)reach=Math.Min(reach,.30f);
float error=(Next()-.5f)*(1-quality)*.3f;
error*=1-.18f*pressureControl;
var side=new Point(-direction.Y,direction.X);
// Aim where the runner will be at the next touch, not where his feet
// are now. Otherwise a fast carrier runs over a decelerating ball.
float interval=Math.Clamp(.24f+(1-quality)*.07f+speed*.012f,.24f,.38f);
if(closeControl)interval=Math.Min(interval,.23f+(1-quality)*.04f);
if(gravity)interval=Math.Min(interval,.18f);
interval*=1-.07f*pressureControl;
var target=p.Position+p.Velocity*interval+direction*reach+side*error;
p.DribbleFoot=Point.Dot(Ball-p.Position,side)>=0?1:-1;
p.DribbleContact=Ball;p.DribbleContactOffset=Ball-p.Position;p.LastDribbleTouch=Seconds;
p.NextDribbleTouch=Seconds+interval;
BallVelocity=(target-Ball)*(2.4f/(1-MathF.Exp(-2.4f*interval)));
if(BallVelocity.Length>12.5f)BallVelocity=BallVelocity.Unit*12.5f;
BallVelocity=LimitDribbleImpulse(BallVelocity);
SnapshotOffside(p,false);
DribbleTouches++;
if(IsMediaLeague)MediaDribbleMistake(p);
}
}