Decision layer for a soccer match simulation. Builds each footballer's movement target from role, possession, tactics, marking, pressing, keeper behavior, skills, duels and direct orders, then passes it through a long chain of positioning modifier methods.
using System;
using System.Linq;
namespace Socer;
// Decision layer only: locomotion, collisions and ball rules remain in MatchSimulation.
public sealed partial class MatchSimulation
{
Point BuildMovementIntent(Footballer p,int possessionTeam,float dt)
{
int dir=Direction(p.Team);
Point target;
if(p.Keeper)
{
p.KeeperRushing=false;
var local=World(Ball,p.Team);
float depth=Math.Clamp(2.4f+Math.Max(0,29-local.X)*.055f,2.4f,4.1f);
// Stay on the ball-to-goal line. A fixed fraction of the ball's
// lateral position over-commits and leaves the far side open.
float lateral=Math.Clamp((local.Y-34)*depth/Math.Max(8,local.X)*.84f,-4.4f,4.4f);
target=World(new Point(depth,34+lateral),p.Team);
if(Owner>=0 && Players[Owner].Team!=p.Team && local.X<21 && Math.Abs(local.Y-34)<19)
{
float step=Math.Clamp(local.X*.28f,2.5f,5.1f);
if(local.X<18 && (SkillActive(p,UniqueSkill.SweeperKeeper) ||
local.X>8 && ActivateSkill(p,UniqueSkill.SweeperKeeper)))step=Math.Min(6.6f,step+1.1f*p.ActiveSkillPower);
float oneOnOne=Math.Clamp((local.Y-34)*step/Math.Max(5,local.X)*(.82f+p.Profile.GkPositioning*.12f),-5,5);
target=World(new Point(step,34+oneOnOne),p.Team);
p.Action="Сокращает угол";
}
if(p.KeeperBlockUntil>Seconds){target=p.Position;p.Action=p.KeeperBlockKind==2?"Раскрывается перед ударом":"Блокирует удар";}
if(Owner==p.Id) target=p.Position;
if(Owner==p.Id)p.Action="Ввод мяча";
else if(p.KeeperBlockUntil<=Seconds && p.Action!="Сокращает угол")p.Action="Страхует ворота";
if(!PassiveTrainingOpponent(p) && TryKeeperClaim(p,out var claimTarget)){target=claimTarget;p.KeeperRushing=true;p.Action="Выход за мячом";}
}
else if(Owner==p.Id)
{
target=p.Position+new Point(dir*8,(34-p.Position.Y)*.3f);
// Wide carriers keep their lane until the final third. The old target
// dragged every winger towards the centre as soon as he received.
float localX=World(p.Position,p.Team).X;
if(localX<97 && Math.Abs(p.Position.Y-34)>18 &&
PerceivedSpace(p,p.Position+new Point(dir*5,0))>2.4f)
target.Y=p.Position.Y+(34-p.Position.Y)*.035f;
p.Action="Ведение";
// A beaten pursuer behind the carrier must not divert an open run.
var forward=(target-p.Position).Unit;
var side=new Point(-forward.Y,forward.X);
foreach(var q in ActivePlayers.Where(q=>q.Team!=p.Team))
{
var seen=Observed(p,q);if(!seen.HasValue)continue;
var delta=seen.Value-p.Position;
float ahead=Point.Dot(delta,forward);
float lateral=Point.Dot(delta,side);
if(ahead>.3f && ahead<5 && Math.Abs(lateral)<2.2f)
target+=side*(lateral>=0?-1:1)*(5-ahead)*.35f;
}
}
else if(chasers[p.Team]==p.Id && (Owner<0 || possessionTeam!=p.Team))
{
target=Ball+BallVelocity*Math.Clamp((p.Position-Ball).Length/25,.1f,.5f);
p.Action=Owner<0?"К мячу":"Прессинг";
}
else
{
bool attack=possessionTeam==p.Team;
float localBall=World(Ball,p.Team).X;
float shift=attack ? 17+(localBall-52)*.5f : (localBall-52)*.32f-5;
shift+=(Tactics[p.Team]-1)*5;
if(attack)shift+=Approach[p.Team]==2?6:Approach[p.Team]==0?-4:0;
else if(Approach[p.Team]==3)shift+=5;
float width=Approach[p.Team]==1 && attack && p.Number>=5?(p.Home.Y<34?-4:4):0;
target=World(new Point(Math.Clamp(p.Home.X+shift,10,97), Math.Clamp(p.Home.Y+width+(World(Ball,p.Team).Y-34)*.18f,4,64)),p.Team);
if(!attack)
{
// Full-backs recover towards the box as the ball approaches it;
// preserve width when play is actually on their touchline.
if(p.Home.X<42 && Math.Abs(p.Home.Y-34)>22 && localBall<38)
{
float central=Math.Clamp(1-Math.Abs(World(Ball,p.Team).Y-34)/23,0,1);
float collapse=Math.Clamp((38-localBall)/25,0,1)*central;
target.Y+=(Math.Clamp(target.Y,19,49)-target.Y)*collapse*.85f;
}
var mark=ActivePlayers.Where(q=>q.Team!=p.Team && !q.Keeper).Select(q=>Observed(p,q)).Where(q=>q.HasValue)
.OrderBy(q=>(q.Value-target).Length).FirstOrDefault();
if(mark.HasValue && (mark.Value-target).Length<15) target=target*.5f+(mark.Value+new Point(-dir*2,0))*.5f;
if(Tactics[p.Team]==2 && (p.Position-Ball).Length<12 && p.Id==secondChasers[p.Team]) { target=Ball; p.Action="Прессинг"; }
}
else if(p.Number>=6) target.Y+=MathF.Sin(Seconds*.6f+p.Number+Seed%19)*2;
}
target=PlanRun(p,target,possessionTeam);
target=SkillTarget(p,target);
target=AdaptDefence(p,target);
if(BallHeight<1) target=LooseBallTarget(p,target);
target=SupportAndPressure(p,target,possessionTeam);
target=FootballMovement(p,target,possessionTeam);
target=CombinationMovement(p,target,possessionTeam);
target=ApplyDefensiveCoordination(p,target,possessionTeam);
target=ApplySecondBallMovement(p,target);
target=CoordinatedDefence(p,target);
target=ProtectCentralBox(p,target,possessionTeam);
target=TacticalMovement(p,target,possessionTeam);
target=BroadcastPlanMovement(p,target,possessionTeam);
target=ApplyCoach(p,target,possessionTeam,dt);
if(Owner<0 && PassReceiver==p.Id && LastTouch==p.Team)
{target=ReceptionMeeting(p);p.Action=BallHeight>1?"Готовится принять навес":"Встречает передачу";}
if(Owner>=0 && Players[Owner].Keeper && p.Team!=possessionTeam && (p.Position-Ball).Length<8)
{
var away=(p.Position-Ball).Unit;
if(away.Length<.01f) away=new Point(-dir,0);
target=Ball+away*8;
p.Action="Отходит от вратаря";
}
if(WindupPlayer==p.Id) { target=p.RunningShot?p.Position+p.Velocity*.3f:p.Position; p.Action=p.RunningShot?"Удар на бегу":"Замах"; }
if(DuelDefender==p.Id) { target=BodyChallenge?Players[DuelCarrier].Position+Players[DuelCarrier].Velocity*.18f+(p.Position-Players[DuelCarrier].Position).Unit*.72f:DuelAim; p.Action=BodyChallenge?"Борется плечом":Sliding?"Подкат":"Отбор стоя"; }
target=StrategyTarget(p,target);
target=AerialMovement(p,target);
if(Owner==p.Id && p.DirectOrder==DirectOrderKind.AttackRun && p.DirectOrderUntil>Seconds &&
WindupPlayer!=p.Id && p.ShotSetupUntil<=0 && Seconds>=p.KickSetupUntil)
{
if((p.Position-p.DirectPoint).Length<1)p.DirectOrder=DirectOrderKind.None;
else target=p.DirectPoint;
}
if(Owner==p.Id && p.FeintVariant is 3 or 4 && Seconds<p.FeintUntil)target=SkillTarget(p,target);
else target=AvoidCarrierObstacle(p,target);
target=CombinationControl(p,target);
target=DribbleMovement(p,target);
if(Owner<0 && Seconds<p.InterceptUntil && p.Team!=LastTouch)
{target=p.InterceptBody?p.InterceptAim:p.Position;p.Action=p.InterceptBody?"Закрывает корпусом":"Выставляет ногу";}
if(Owner<0 && Seconds<p.RecoveryChaseUntil && LastTouch==p.Team)
{target=LooseBallMeeting(p);p.Action="Исправляет приём";}
target=ApplyDirectOrder(p,target,possessionTeam);
if(Owner==p.Id && Seconds<p.KickSetupUntil && BallAtFoot(p))target=p.Position;
target=SafeCarrierMovement(p,target);
target=RoleMovement(p,target,possessionTeam);
target=LinkedAttackMovement(p,target,possessionTeam);
target=LoadoutMovement(p,target,possessionTeam);
target=LegendaryMovement(p,target,possessionTeam);
target=PreserveCombinationRun(p,target,possessionTeam);
target=Clamp(target);
return target;
}
}