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