FinalThirdPlay.cs

Part of the match simulation that handles off-ball attacking movement in the final third. Picks near-post, far-post and cutback runners, adjusts their target pocket based on perceived space, and assigns rebound positions when a shot is in flight.

using System;
using System.Linq;

namespace Socer;

public sealed partial class MatchSimulation
{
    public int KeeperParries,KeeperCornerTips,KeeperTouchlineTips,KeeperSpills,ReboundShots;
    int secondChanceTeam=-1;
    float secondChanceUntil;
    readonly int[][] finalThirdRoles={new[]{-1,-1,-1},new[]{-1,-1,-1}};
    readonly int[] finalThirdCarrier={-1,-1};
    readonly float[] finalThirdRolesUntil=new float[2];

    // Arrive in a deliverable pocket instead of camping on the six-yard line.
    // The opposite winger can attack the far post; a midfielder stays behind
    // the first line for cutbacks and rebounds. Explicit orders still win later.
    bool TryFinalThirdMovement(Footballer p,int possessionTeam,out Point target)
    {
        target=p.Position;
        if(possessionTeam!=p.Team || !(Owner>=0 || ShotInFlight))return false;
        var ball=World(Ball,p.Team);bool wide=Math.Abs(ball.Y-34)>12;
        if(ball.X<(wide?72:79))return false;
        if(ShotInFlight)
        {
            if(p.BoxRole<=0)return false;
            var landing=Clamp(Ball+BallVelocity*.2f);
            target=Clamp(landing+new Point(-Direction(p.Team)*(p.BoxRole==3?4:1.2f),p.BoxRole==2?3:-2));
            p.Action=p.BoxRole==3?"Страхует отскок у штрафной":"Открывается на добивание";
            return true;
        }
        float flank=ball.Y<34?-1:1;
        var roles=finalThirdRoles[p.Team];
        if(Seconds>=finalThirdRolesUntil[p.Team] || finalThirdCarrier[p.Team]!=Owner ||
            roles.Any(id=>id>=0 && (Players[id].SentOff || Players[id].Stamina<.2f)))
        {
            finalThirdRolesUntil[p.Team]=Seconds+.65f;finalThirdCarrier[p.Team]=Owner;
            var candidates=ActivePlayers.Where(q=>q.Team==p.Team && q.Id!=Owner && AttackUnit(q) &&
                Seconds>=q.FallUntil && q.Stamina>.2f &&
                !(wide && Math.Abs(q.Home.Y-34)>17 && Math.Sign(q.Home.Y-34)==flank)).ToArray();
            var first=candidates.OrderBy(q=>(World(q.Position,p.Team)-new Point(93,34+flank*4)).Length-
                (StrikerRole(q)?9:0)-q.Profile.OffBall*3).FirstOrDefault();
            var second=candidates.Where(q=>q!=first).OrderBy(q=>(World(q.Position,p.Team)-new Point(92,34-flank*5)).Length-
                (StrikerRole(q)?7:0)-(Math.Abs(q.Home.Y-34)>17?10:0)-q.Profile.OffBall*3).FirstOrDefault();
            var third=candidates.Where(q=>q!=first && q!=second).OrderBy(q=>(World(q.Position,p.Team)-new Point(85,34)).Length+
                (StrikerRole(q)?8:0)+Math.Abs(q.Home.Y-34)*.2f).FirstOrDefault();
            roles[0]=first?.Id??-1;roles[1]=second?.Id??-1;roles[2]=third?.Id??-1;
        }
        int slot=Array.IndexOf(roles,p.Id);if(slot<0)return false;
        if(p.BoxRole!=slot+1)
        {BoxRuns++;if(slot==0)NearPostRuns++;else if(slot==1)FarPostRuns++;else CutbackRuns++;}
        p.BoxRole=slot+1;
        float x=slot==0?Math.Clamp(ball.X+9,88,97):slot==1?Math.Clamp(ball.X+6,88,96):Math.Clamp(ball.X-6,82,90);
        float y=34+flank*(slot==0?3.6f:slot==1?-4.8f:1.8f);
        var ideal=World(new Point(x,y),p.Team);target=ideal;float best=float.MinValue;
        // Better off-ball awareness recognises a free shoulder beside the marker.
        foreach(float offset in new[]{0f,-2.6f,2.6f})
        {
            var pocket=OnsideRunTarget(p,Clamp(ideal+new Point(-Direction(p.Team)*Math.Abs(offset)*.35f,offset)));
            float value=Math.Min(6,PerceivedSpace(p,pocket))*(.6f+p.Profile.OffBall)-Math.Abs(offset)*.55f;
            if(value>best){best=value;target=pocket;}
        }
        p.Action=slot==0?"Рывок в ближний коридор":slot==1?"Рывок к дальней штанге":"Открывается под пас назад";
        return true;
    }
}