Part of the soccer match simulation. Assigns man-marking targets, calls keeper line shifts, moves defenders into central box coverage, and sends nearby players to chase second balls.
using System;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
public int[] MarkHandoffs=new int[2],KeeperLineCalls=new int[2];
public int KeeperWallCalls,SecondBallChases;
public float[] KeeperLineUntil=new float[2],KeeperLineShift=new float[2];
float nextMarking, nextKeeperDirection;
void UpdateDefensiveCoordination()
{
if(Seconds>=nextMarking)
{
nextMarking=Seconds+.32f;
for(int team=0;team<2;team++)
{
int possession=Owner>=0?Players[Owner].Team:LastTouch;
if(possession==team || RestartRemaining>0 || GoalHoldRemaining>0)
{foreach(var p in ActivePlayers.Where(p=>p.Team==team))p.AutoMarkId=-1;continue;}
var defenders=ActivePlayers.Where(p=>p.Team==team && !p.Keeper && p.Id!=chasers[team] &&
p.DirectOrder==DirectOrderKind.None && Seconds>=p.FallUntil).ToList();
var attackers=ActivePlayers.Where(p=>p.Team!=team && !p.Keeper && p.Id!=Owner &&
World(p.Position,team).X<77 && (p.Position-Ball).Length<36 &&
// The ball presser handles a player by the corner. Box defenders
// stay available for a cross, cutback and the far-post runner.
!(World(Ball,team).X<36 && World(p.Position,team).X<36 &&
Math.Abs(World(p.Position,team).Y-34)>18))
.OrderBy(p=>World(p.Position,team).X+Math.Abs(p.Position.Y-34)*.55f-(p.Id==PassReceiver?12:0)).Take(5).ToArray();
var previous=defenders.ToDictionary(p=>p.Id,p=>p.AutoMarkId);
foreach(var p in defenders)p.AutoMarkId=-1;
foreach(var attacker in attackers)
{
// A field call already assigned this receiver; keep other defenders available.
if(ActivePlayers.Any(p=>p.Team==team && p.DirectOrderUntil>Seconds &&
p.DirectTargetId==attacker.Id && p.DirectOrder is DirectOrderKind.MarkPlayer or DirectOrderKind.PressPlayer))continue;
var goal=World(new Point(0,34),team);
var anchor=attacker.Position+(goal-attacker.Position).Unit*1.1f;
var chosen=defenders.Where(d=>Observed(d,attacker).HasValue).OrderBy(d=>(d.Position-anchor).Length+
(previous[d.Id]==attacker.Id?-2.2f:0)+Math.Abs(d.Home.Y-attacker.Home.Y)*.015f).FirstOrDefault();
if(chosen==null || (chosen.Position-attacker.Position).Length>25)continue;
if(previous[chosen.Id]!=attacker.Id && previous.Values.Contains(attacker.Id))MarkHandoffs[team]++;
chosen.AutoMarkId=attacker.Id;defenders.Remove(chosen);
}
}
}
if(Seconds<nextKeeperDirection || Owner<0)return;
nextKeeperDirection=Seconds+3.5f;
for(int team=0;team<2;team++)
{
if(Players[Owner].Team==team)continue;
float danger=World(Ball,team).X;
if(danger>49)continue;
var keeper=ActivePlayers.FirstOrDefault(p=>p.Team==team && p.Keeper);
if(keeper==null)continue;
KeeperLineShift[team]=danger<28?-2.5f:1.8f;
KeeperLineUntil[team]=Seconds+3.8f;KeeperLineCalls[team]++;
Say(keeper,danger<28?"Линия ниже!":"Линия выше!",false);
}
}
Point ApplyDefensiveCoordination(Footballer p,Point target,int possessionTeam)
{
if(p.Keeper || p.DirectOrder!=DirectOrderKind.None || WindupPlayer==p.Id || DuelDefender==p.Id)return target;
if(possessionTeam!=p.Team && BackLine(p) && KeeperLineUntil[p.Team]>Seconds)
target.X+=Direction(p.Team)*KeeperLineShift[p.Team];
if(possessionTeam!=p.Team && p.AutoMarkId>=0 && p.Id!=chasers[p.Team])
{
var runner=Players[p.AutoMarkId];
var seen=Observed(p,runner);
if(runner.InPlay && runner.Id!=Owner && seen.HasValue && (seen.Value-p.Position).Length<26)
{
var goal=World(new Point(0,34),p.Team);
bool dangerous=(seen.Value-goal).Length<32;
var cover=seen.Value+(goal-seen.Value).Unit*(dangerous?1.05f:1.6f);
if(CanObserve(p,runner))
{
float awareness=p.Profile.Defence*p.Profile.RoleFamiliarity;
cover+=runner.Velocity*(.20f-(1-awareness)*(1-awareness)*.55f);
}
p.Action=dangerous?"Закрывает удар подопечного":"Сопровождает открывание";
p.DecisionReason=runner.Id==PassReceiver?"Пас идёт подопечному · сближается со стороны ворот":"Держит нападающего перед собой и закрывает путь к воротам";
return dangerous?Clamp(cover):Clamp(target*.12f+cover*.88f);
}
}
return target;
}
Point ProtectCentralBox(Footballer p,Point target,int possessionTeam)
{
if(possessionTeam==p.Team || p.Keeper || Owner<0 || Players[Owner].Keeper ||
p.DirectOrder!=DirectOrderKind.None || p.Id==chasers[p.Team] ||
DuelDefender==p.Id || Seconds<p.FallUntil || p.BlockUntil>Seconds)return target;
var ball=World(Ball,p.Team);
if(ball.X>=36)return target;
if(p.AutoMarkId>=0 && p.AutoMarkId!=Owner && Players[p.AutoMarkId].InPlay)
{
var runner=World(Players[p.AutoMarkId].Position,p.Team);
if(runner.X<34 && Math.Abs(runner.Y-34)<18)return target;
}
bool backLine=p.Home.X<27 || p.Home.X<42 && Math.Abs(p.Home.Y-34)>23;
if(backLine)
{
var line=ActivePlayers.Where(q=>q.Team==p.Team && !q.Keeper &&
(q.Home.X<27 || q.Home.X<42 && Math.Abs(q.Home.Y-34)>23))
.OrderBy(q=>q.Home.Y).ToArray();
int slot=Array.FindIndex(line,q=>q.Id==p.Id);
float span=line.Length>=5?28:22;
float lane=34-span*.5f+Math.Max(0,slot)*span/Math.Max(1,line.Length-1);
float ballSide=Math.Clamp((ball.Y-34)*.12f,-2.5f,2.5f);
var cover=World(new Point(Math.Clamp(ball.X-3.2f,3.5f,24),lane+ballSide),p.Team);
float urgency=Math.Clamp((36-ball.X)/14,.25f,.9f);
p.Action="Защищает штрафную";
return Clamp(target*(1-urgency)+cover*urgency);
}
if(AttackUnit(p) && !StrikerRole(p))
{
var local=World(target,p.Team);
local.X=Math.Clamp(local.X,Math.Max(16,ball.X+3),Math.Max(23,ball.X+11));
local.Y=Math.Clamp(local.Y,17,51);
return World(local,p.Team);
}
return target;
}
void PrepareSecondBall(Footballer clearer,Point landing,float duration=2.6f)
{
for(int team=0;team<2;team++)
{
int slot=0;
foreach(var p in ActivePlayers.Where(p=>p.Team==team && !p.Keeper && p.Id!=clearer.Id)
.OrderBy(p=>(p.Position-landing).Length).Take(2))
{
p.SecondBallPoint=Clamp(landing+new Point(team==clearer.Team?-2:2,slot==0?-1.5f:1.5f));
p.SecondBallUntil=Seconds+duration;
slot++;SecondBallChases++;
}
}
}
Point ApplySecondBallMovement(Footballer p,Point target)
{
if(p.Keeper || Owner>=0 || BallSpeed<3 || p.SecondBallUntil<=Seconds ||
WindupPlayer==p.Id || DuelDefender==p.Id)return target;
p.Action="Идёт на второй мяч";
// The landing point is useful while the ball is high. After a ground
// parry, keep tracking its actual deceleration instead of camping at an
// old predicted point while it rolls several metres past the runner.
return BallHeight<1 && VerticalSpeed<2?LooseBallMeeting(p):p.SecondBallPoint;
}
}