AdvancedPlay.cs

Part of the football match simulation: the scissors-feint dribble move and its two-touch ball handling, plus aerial contests (headers, chest control, knock-downs, aerial duels) and off-ball target selection for pressing, pass blocking, shot blocking and meeting high balls.

Rough Code
using System;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
    public int Headers,HeadedShots,ChestControls,DribbleAttempts,DribblesWon,ShotBlocks,AerialDuels;
    // Deliberately frequent for the current live-match playtest; isolated from normal dribble odds.
    public bool FrequentScissors=true;
    public int ScissorsAttempts,ScissorsCuts;
    public bool TryScissorsFeint(Footballer p)
    {
        if(Owner!=p.Id || p.Keeper || RehearsingStrategy || Strategy is {Done:false} || !CanFieldCall ||
            WindupPlayer>=0 || p.ShotSetupUntil>0 || Seconds<p.KickSetupUntil || Seconds<p.FeintReadyAt ||
            Seconds<p.FeintUntil || Seconds<p.ReceiveUntil || Seconds<p.ChestUntil || Seconds<p.FallUntil ||
            Seconds-p.LastSlideAt<2.1f || p.Held<.35f || p.Stamina<.25f || !BallAtFoot(p) ||
            p.Position.X<8 || p.Position.X>97 || p.Position.Y<3 || p.Position.Y>65 || ClearFinishingChance(p))return false;
        var axis=p.Velocity.Length>1?p.Velocity.Unit:new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing));
        var q=ActivePlayers.Where(q=>q.Team!=p.Team && !q.Keeper && Seconds>=q.FallUntil &&
            (q.Position-p.Position).Length is >1.4f and <4.5f && Point.Dot((q.Position-p.Position).Unit,axis)>.35f)
            .OrderBy(q=>(q.Position-p.Position).Length).FirstOrDefault();
        if(q==null)return false;
        p.FeintReadyAt=Seconds+(FrequentScissors?2.8f:5.5f);
        if(Next()>(FrequentScissors?.88f:.22f+p.Skill*.28f))return false;
        var lateral=new Point(-axis.Y,axis.X);
        int side=Point.Dot(q.Position-p.Position,lateral)>0?-1:1;
        var exit=(axis*.65f+lateral*side).Unit;
        if(CarryLane(p,p.Position+exit*3)<1.05f)
        {side=-side;exit=(axis*.65f+lateral*side).Unit;}
        if(CarryLane(p,p.Position+exit*3)<.8f)return false;
        p.ScissorsAxis=axis;p.ScissorsSide=side;p.ScissorsStage=0;p.ScissorsOpponent=q.Id;
        p.FeintVariant=3;p.FeintDirection=exit;p.FeintStartedAt=Seconds;p.FeintUntil=Seconds+.85f;
        p.CarryAvoidUntil=0;p.PlanUntil=0;p.ShieldUntil=0;p.Stamina-=.014f;
        if(!ActivateSkill(p,UniqueSkill.PhantomCut,1.5f))ActivateSkill(p,UniqueSkill.Counterstep,1.5f);
        ScissorsAttempts++;DribbleAttempts++;DribbleAttemptsByTeam[p.Team]++;p.Action="Ножницы · ложный шаг";
        Emit("dribble",p,"НОЖНИЦЫ",.85f);
        return true;
    }
    bool AdvanceScissorsTouch(Footballer p,float gap)
    {
        if(p.FeintVariant!=3 || Seconds>=p.FeintUntil)return false;
        float age=Seconds-p.FeintStartedAt;
        // The boot may only change velocity at a reachable contact; never relocate the ball.
        if(gap>DribbleFootReach)return true;
        var side=new Point(-p.ScissorsAxis.Y,p.ScissorsAxis.X)*p.ScissorsSide;
        if(p.ScissorsStage==0 && age>=.12f)
        {
            BallVelocity=LimitDribbleImpulse(p.Velocity*.35f+p.ScissorsAxis*.6f-side*.9f);
            p.ScissorsStage=1;p.DribbleFoot=-p.ScissorsSide;
        }
        else if(p.ScissorsStage==1 && age>=.40f)
        {
            BallVelocity=LimitDribbleImpulse(p.FeintDirection*(3.6f+p.Skill*1.2f));
            p.ScissorsStage=2;p.DribbleFoot=p.ScissorsSide;ScissorsCuts++;
            p.BurstRemaining=Math.Max(p.BurstRemaining,1.25f);p.RunUntil=Seconds+1.2f;
            p.RunTarget=Clamp(p.Position+p.FeintDirection*5);
            var q=Players[p.ScissorsOpponent];
            var cue=p.FeintDirection.Y<0?AttackCue.DribbleLeft:AttackCue.DribbleRight;
            float known=OpponentMemory.Recognition(q.Id,p.Id,cue,Seconds);
            if((q.Position-p.Position).Length<4 && Next()<Math.Clamp(.48f+(p.Skill-q.Profile.Defence)*.35f-known*.25f+
                (SkillActive(p,UniqueSkill.PhantomCut) || SkillActive(p,UniqueSkill.Counterstep)?.20f*p.ActiveSkillPower:0),.18f,.87f))
            {
                float beaten=.16f+(1-q.Profile.Defence)*.16f;
                q.Recovery=Math.Max(q.Recovery,beaten);
                q.StumbleUntil=Seconds+beaten;
                DribblesWon++;DribblesWonByTeam[p.Team]++;
            }
            OpponentMemory.Observe(Players,p,cue,Seconds);
        }
        else return p.ScissorsStage<2;
        SnapshotOffside(p,false);
        p.DribbleContact=Ball;p.DribbleContactOffset=Ball-p.Position;p.LastDribbleTouch=Seconds;p.NextDribbleTouch=Seconds+.22f;DribbleTouches++;
        return true;
    }
    Point SkillTarget(Footballer p,Point target)
    {
        if(p.Keeper)return target;
        if(Owner==p.Id && Seconds<p.ChestUntil){p.Action="Приём грудью";return p.ReceiveOnMove?p.Position+p.ReceiveDirection*2:p.Position;}
        if(Owner==p.Id && Seconds<p.FeintUntil)
        {
            if(p.FeintVariant==3)
            {
                bool probe=p.ScissorsStage<2;
                p.Action=probe?"Ножницы · ложный шаг":"Ножницы · рывок в сторону";
                return Clamp(p.Position+(probe?p.ScissorsAxis*.4f:p.FeintDirection*4));
            }
            if(p.FeintVariant==4)
            {
                p.Action=SkillMoves.Get(p.SkillMove).Name;
                var exit=SkillExit(p);
                return Clamp(p.Position+(p.SkillStage==0?p.SkillAxis*.35f:p.SkillStage==1?exit*1.6f:exit*4));
            }
            bool probeOld=Seconds-p.FeintStartedAt<.16f;p.Action=probeOld?"Провоцирует защитника":"Обводка с ускорением";return Clamp(p.Position+p.FeintDirection*(probeOld?.35f:4));
        }
        if(Owner>=0 && Players[Owner].Team!=p.Team)
        {
            var carrier=Players[Owner];var delta=carrier.Position-p.Position;
            if(p.Id==chasers[p.Team] && !carrier.Keeper)
            {
                // Approach from the dangerous passing side, then close down the actual ball.
                var goal=World(new Point(0,34),p.Team);
                target=delta.Length>3?carrier.Position+(goal-carrier.Position).Unit*1.2f:Ball;
                p.Action="Давит на мяч";
            }
            bool pressing=Tactics[p.Team]==2 || (p.Team==0 && CoachOrder==1 && Seconds<CoachUntil);
            if(pressing && p.Id==secondChasers[p.Team] && delta.Length<16)
            {
                var receiver=ActivePlayers.Where(q=>q.Team==carrier.Team && q.Id!=carrier.Id && !q.Keeper).Select(q=>Observed(p,q)).Where(q=>q.HasValue)
                    .OrderBy(q=>(q.Value-carrier.Position).Length).FirstOrDefault();
                if(receiver.HasValue){target=carrier.Position+(receiver.Value-carrier.Position).Unit*3;p.Action="Перекрывает пас";}
            }
        }
        if((WindupPlayer>=0 || ShotInFlight) && LastTouch!=p.Team && World(Ball,p.Team).X<35 && (Ball-p.Position).Length<14)
        {
            var goal=World(new Point(0,34),p.Team);var axis=(goal-Ball).Unit;
            var blockers=ActivePlayers.Where(q=>q.Team==p.Team&&!q.Keeper).OrderBy(q=>DistanceToSegment(q.Position,Ball,goal)).Take(2);
            if(blockers.Any(q=>q.Id==p.Id))
            {float ahead=Math.Clamp(Point.Dot(p.Position-Ball,axis),1.7f,10);target=Ball+axis*ahead;p.BlockUntil=Seconds+.25f;p.Action="Блокирует удар";}
        }
        if(Owner<0 && BallHeight>1 && !ShotInFlight)
        {
            float discriminant=VerticalSpeed*VerticalSpeed+19.62f*(BallHeight-1.9f);
            float arrival=discriminant>0?Math.Clamp((VerticalSpeed+MathF.Sqrt(discriminant))/9.81f,0,1.5f):.15f;
            var meeting=Clamp(Ball+BallVelocity*arrival);
            if((meeting-p.Position).Length<20 && ActivePlayers.Where(q=>q.Team==p.Team&&!q.Keeper).OrderBy(q=>(q.Position-meeting).Length).Take(2).Any(q=>q.Id==p.Id))
            {target=meeting;p.Action="Под верховой мяч";}
        }
        return target;
    }
    bool TryDribble(Footballer p)=>TrySkillMove(p);
    bool TryAerialContact(Point before,float beforeHeight)
    {
        if(PickupDelay>0 || Math.Max(beforeHeight,BallHeight)<1 || Math.Min(beforeHeight,BallHeight)>2.6f)return false;
        var delta=Ball-before;float length2=Point.Dot(delta,delta);
        Point HeadAnchor(Footballer p)=>p.Position+new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing))*.18f;
        float ContactT(Footballer p)=>length2<.001f?1:Math.Clamp(Point.Dot(HeadAnchor(p)-before,delta)/length2,0,1);
        float HeightAt(Footballer p){float t=ContactT(p);return beforeHeight+(BallHeight-beforeHeight)*t;}
        float HeadDistance(Footballer p)=>(before+delta*ContactT(p)-HeadAnchor(p)).Length;
        var candidates=ActivePlayers.Where(p=>!p.Keeper && !p.IsAcrobaticCommitted(Seconds) && !PassiveTrainingOpponent(p) && FirstTimePhase(p)<0 && Seconds>=p.FallUntil && !((p.Id==Passer || shot && p.Id==lastShooter) && flightAge<.35f) && Seconds>=p.AerialReadyAt &&
            HeightAt(p)>=1 && HeightAt(p)<=1.88f+p.Profile.Jumping*.5f &&
            (HeightAt(p)<=2.1f || Seconds-p.AerialContestAt<.35f) &&
            HeadDistance(p)<.26f+p.Profile.HeadingAccuracy*.15f)
            .OrderByDescending(p=>p.Profile.HeadingAccuracy*.32f+p.Profile.Jumping*.24f+
                p.Profile.Physical*.16f+p.Profile.OffBall*.12f+p.Stamina*.08f+
                (PlayerPlayStyles.Has(p,PlayStyle.Aerial)?.12f:0)-HeadDistance(p)*.5f).ToArray();
        if(candidates.Length==0)return false;
        var winner=candidates[0];
        if(candidates.Any(p=>p.Team!=winner.Team && (p.Position-winner.Position).Length<1.5f))
        {
            AerialDuels++;
            foreach(var contender in candidates.Where(p=>(p.Position-winner.Position).Length<1.5f))
            {contender.AerialContestAt=Seconds;contender.Action="Борется за верховой мяч";}
        }
        float height=HeightAt(winner);bool pressured=candidates.Any(p=>p.Team!=winner.Team) || ActivePlayers.Any(p=>p.Team!=winner.Team && (p.Position-winner.Position).Length<2);
        bool defending=winner.Team!=LastTouch && (shot || World(winner.Position,winner.Team).X<30);
        bool headAtGoal=!defending && World(winner.Position,winner.Team).X>84 && Math.Abs(winner.Position.Y-34)<15 && height>1.6f;
        int incomingPasser=Passer;
        if(winner.Team!=LastTouch && height<1.55f)
        {
            if(!TryBodyContact(winner,before,beforeHeight,out var bodyContact,out float bodyHeight,out var bodyNormal))return false;
            if(CheckOffside(winner) || CheckOffsideInterference(winner,before,beforeHeight))return true;
            Ball=bodyContact;BallHeight=bodyHeight;ReboundInterception(winner,bodyNormal);return true;
        }
        var outlet=ActivePlayers.Where(q=>q.Team==winner.Team && q.Id!=winner.Id && !q.Keeper &&
            (q.Position-winner.Position).Length is >3 and <20 && SeesPartner(winner,q) && IsOnside(q,winner.Position) &&
            PerceivedLane(winner,q.Position)>1 && PerceivedSpace(winner,q.Position)>2)
            .OrderByDescending(q=>(q.Position.X-winner.Position.X)*Direction(winner.Team)*.5f+Math.Min(6,PerceivedSpace(winner,q.Position))).FirstOrDefault();
        bool headPass=!defending && !headAtGoal && height>=1.55f && outlet!=null &&
            (pressured || (outlet.Position.X-winner.Position.X)*Direction(winner.Team)>3) && winner.Profile.Vision>.35f;
        // An uncontested delivery is a receipt, not an obligatory blind header.
        if(!shot && winner.Team==LastTouch && !pressured && !headAtGoal && !headPass && height>=1.65f)return false;
        if(!shot && height<1.65f && !pressured && !headPass && VerticalSpeed<1)
        {
            if(CheckOffside(winner) || CheckOffsideInterference(winner,before,beforeHeight))return true;
            TakeBall(winner);winner.ReceiveDirection=ChooseFirstTouch(winner);
            winner.ReceiveOnMove=winner.Skill>.6f && winner.Velocity.Length>1.5f;
            winner.ChestUntil=Seconds+(winner.ReceiveOnMove?.22f:.32f+(1-winner.Skill)*.18f);winner.ChestHeight=height;winner.AerialReadyAt=Seconds+.6f;
            BallVelocity=winner.ReceiveOnMove?winner.ReceiveDirection*(1.5f+winner.Velocity.Length*.35f):new Point();
            BallHeight=height;ChestControls++;winner.Action="Приём грудью";Emit("control",winner,"ГРУДЬЮ",1.2f);AddEvent($"{TeamName(winner.Team)} · принял грудью #{winner.Number}");return true;
        }
        if(height<1.55f)return false;
        if(CheckOffside(winner) || CheckOffsideInterference(winner,before,beforeHeight))return true;
        RegisterShotContact("ВЕРХОВОЙ КОНТАКТ");
        if(incomingPasser>=0 && Players[incomingPasser].Team==winner.Team && incomingPasser!=winner.Id)Completed[winner.Team]++;
        var mate=outlet??ActivePlayers.Where(p=>p.Team==winner.Team && p.Id!=winner.Id&&!p.Keeper).OrderBy(p=>(p.Position-winner.Position).Length).First();
        Point target=headAtGoal?World(new Point(105,34),winner.Team):
            defending?Clamp(winner.Position+new Point(Direction(winner.Team)*20,winner.Position.Y<34?-12:12)):PassMeetingPoint(winner,mate);
        if(!headAtGoal && !defending)
        {
            float error=(1-winner.Skill)*(pressured?2.8f:1.2f);
            target=Clamp(target+new Point((Next()-.5f)*error,(Next()-.5f)*error));
        }
        Point contact=before+delta*ContactT(winner);
        float headerSpeed=headAtGoal?17+winner.Profile.Finishing*5:defending?18:12;
        float headerVertical=headAtGoal?-2.2f:defending?3:0;
        if(headAtGoal)
        {
            var finish=HeaderFinish(winner,contact,height,pressured);
            target=finish.Target;headerSpeed=finish.Speed;headerVertical=finish.Vertical;
        }
        bool goalSave=winner.Team!=LastTouch && ShotTowardsGoal(winner.Team);
        Ball=contact;
        Kick(winner,target,headerSpeed,headAtGoal,preserveOppositionOffside:goalSave);
        // A visual tail from the previous kick cannot grant another scissor.
        bool royalStrike=headAtGoal && LegendaryActive(winner,LegendaryAbility.RoyalReception) &&
            winner.LegendaryActivatedAt==Seconds;
        Strike=royalStrike?StrikeKind.Driven:StrikeKind.Header;winner.LastKickKind=Strike;winner.HeaderAt=Seconds;winner.AerialReadyAt=Seconds+.7f;
        winner.HeaderContactPoint=contact;winner.HeaderContactHeight=height;winner.HeaderLeap=Math.Clamp(height-1.72f,0,.40f);
        if(royalStrike)BeginAcrobaticRecovery(winner);
        Ball=contact+(target-contact).Unit*.11f;
        BallVelocity=(target-Ball).Unit*headerSpeed;
        BallHeight=height;VerticalSpeed=headerVertical;BallSpin=0;if(!royalStrike)Headers++;
        if(headAtGoal && SkillActive(winner,UniqueSkill.AerialFinisher))
        {BallUniqueSkill=UniqueSkill.AerialFinisher;BallSkillGrade=winner.ActiveSkillGrade;}
        if(headAtGoal && (royalStrike || LegendaryActive(winner,LegendaryAbility.SkySiu)))BallLegendaryAbility=winner.ActiveLegendaryAbility;
        if(headAtGoal){Shots[winner.Team]++;if(royalStrike)LegendaryAcrobaticShots++;else HeadedShots++;ShotCueRemaining=Math.Clamp((target-Ball).Length/BallSpeed+.35f,.55f,2.5f);}
        else Passes[winner.Team]++;
        if(!headAtGoal && !defending){PassReceiver=mate.Id;IntendedPassTarget=target;}
        winner.Action=royalStrike?"Акробатический удар":headAtGoal?"Удар головой":"Скидка головой";
        Emit("header",winner,defending?"ВЫНЕС ГОЛОВОЙ":royalStrike?"КОРОЛЕВСКИЙ ПРИЁМ":headAtGoal?"ГОЛОВОЙ!":"СКИДКА",1.4f);
        AddEvent($"{TeamName(winner.Team)} · {winner.Action.ToLowerInvariant()} #{winner.Number}");return true;
    }
}