DribbleTouches.cs

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