Part of MatchSimulation for a soccer game. Plans linked off-ball runs and passes between nearby teammates, validates the route and passing lane, adjusts reception timing and facing, and tracks link run, pass and reception counters per team.
using System;
namespace Socer;
public sealed partial class MatchSimulation
{
readonly float[] linkedRunUntil=new float[22],nextLinkedRun=new float[22],nextLinkedValidation=new float[22];
readonly int[] linkedRunPasser=new int[22];
readonly Point[] linkedRunTarget=new Point[22];
public int[] SkillLinkRuns=new int[2],SkillLinkPasses=new int[2],SkillLinkReceptions=new int[2];
public int[] SkillLinkCombinations=new int[2],SkillLinkCompletions=new int[2];
public float[] SkillLinkProgress=new float[2];
int linkedPasser=-1,linkedReceiver=-1;
float linkedPassUntil;
Point linkedPassOrigin;
float PassingRunLink(Footballer passer,Footballer runner)
{
if(!SkillNeighbors(passer,runner) || passer.Profile.RoleFamiliarity<.7f ||
runner.Profile.RoleFamiliarity<.7f || Seconds<passer.FallUntil || Seconds<runner.FallUntil)return 0;
float delivery=Math.Max(SkillStrength(passer,UniqueSkill.ThreadNeedle),
Math.Max(SkillStrength(passer,UniqueSkill.LongDistributor),SkillStrength(passer,UniqueSkill.KeeperDistributor)));
float movement=Math.Max(SkillStrength(runner,UniqueSkill.SpaceHunter),SkillStrength(runner,UniqueSkill.OutletRunner));
return Math.Min(delivery,movement);
}
float WallRunLink(Footballer passer,Footballer runner)
{
if(!SkillNeighbors(passer,runner) || passer.Profile.RoleFamiliarity<.7f || runner.Profile.RoleFamiliarity<.7f)return 0;
return Math.Min(SkillStrength(passer,UniqueSkill.WallPass),Math.Max(SkillStrength(runner,UniqueSkill.SpaceHunter),
Math.Max(SkillStrength(runner,UniqueSkill.Poacher),SkillStrength(runner,UniqueSkill.OutletRunner))));
}
// Plan at the normal thinking cadence, then hold a readable route. Only a
// visible, safe lane can create an opening; this is not a global speed buff.
bool TryPlanLinkedRun(Footballer p,Footballer carrier)
{
float link=PassingRunLink(carrier,p);
if(link<=0 || carrier.Keeper || p.Stamina<.3f || p.DirectOrderUntil>Seconds ||
Seconds<nextLinkedRun[p.Id] || (p.Position-carrier.Position).Length is <6 or >32 ||
World(carrier.Position,p.Team).X is <30 or >81 || !SeesPartner(carrier,p) ||
!IsOnside(p,carrier.Position) || !AttackUnit(p))return false;
var play=combinations[p.Team];
if(play.Stage>0 && Seconds<play.Until && (play.Starter==p.Id || play.Third==p.Id))return false;
nextLinkedRun[p.Id]=Seconds+2.4f;
float side=p.Position.Y<carrier.Position.Y?-1:1,dir=Direction(p.Team);
Point best=p.Position;float value=float.MinValue;
for(int option=0;option<4;option++)
{
float depth=option<2?10:15,lateral=(option%2==0?4:8)*side;
var at=OnsideRunTarget(p,carrier.Position+new Point(dir*depth,lateral));
float gain=(at.X-p.Position.X)*dir;
if(gain<2 || (at-p.Position).Length>14)continue;
float space=PerceivedSpace(p,at),lane=PerceivedLane(carrier,at);
if(space<2.6f || lane<1.35f || PassSafety(carrier,p,at)<.56f)continue;
bool occupied=false;
for(int i=0;i<Players.Count;i++)
{
var teammate=Players[i];
if(teammate.Team==p.Team && teammate.Id!=p.Id && teammate.Id!=carrier.Id && !teammate.SentOff &&
(linkedRunUntil[i]>Seconds && (linkedRunTarget[i]-at).Length<3 ||
(teammate.Position-at).Length<2.4f)){occupied=true;break;}
}
if(occupied)continue;
float score=Math.Min(space,6)*1.1f+Math.Min(lane,4)*1.5f+Math.Min(gain,9)*.28f-(at-p.Position).Length*.12f;
if(score>value){best=at;value=score;}
}
if(value==float.MinValue)return false;
linkedRunTarget[p.Id]=best;linkedRunPasser[p.Id]=carrier.Id;
linkedRunUntil[p.Id]=Seconds+2.1f+.25f*link;
nextLinkedValidation[p.Id]=Seconds+.25f;
p.FootballRun=FootballRun.Behind;p.FootballRunTarget=best;
p.FootballRunStarted=Seconds;p.FootballRunUntil=linkedRunUntil[p.Id];
SkillLinkRuns[p.Team]++;IntelligentRuns++;RunTypes[(int)FootballRun.Behind]++;
return true;
}
Point LinkedAttackMovement(Footballer p,Point target,int possessionTeam)
{
if(possessionTeam!=p.Team || Owner<0 || Owner!=linkedRunPasser[p.Id] || Owner==p.Id ||
Seconds>=linkedRunUntil[p.Id] || p.DirectOrderUntil>Seconds || WindupPlayer==p.Id ||
DuelDefender==p.Id || Seconds<p.FallUntil || PassingRunLink(Players[Owner],p)<=0)return target;
if(Seconds>=nextLinkedValidation[p.Id])
{
nextLinkedValidation[p.Id]=Seconds+.25f;
if(PerceivedSpace(p,linkedRunTarget[p.Id])<1.9f ||
PassSafety(Players[Owner],p,linkedRunTarget[p.Id])<.42f)
{
linkedRunUntil[p.Id]=Seconds;p.FootballRun=FootballRun.None;p.FootballRunUntil=Seconds+.5f;
return target;
}
}
p.Action="Открывается под связанный пас";
return OnsideRunTarget(p,linkedRunTarget[p.Id]);
}
float LinkedPassValue(Footballer p,Footballer q,float gain,float safety,float space)
{
if(gain<3 || safety<.56f || space<2.6f ||
!(q.RunUntil>Seconds || q.FootballRunUntil>Seconds && q.FootballRun is FootballRun.Behind or FootballRun.Overlap))return 0;
return PassingRunLink(p,q)*2.8f;
}
void RecordLinkedPass(Footballer p,Footballer q)
{
linkedPasser=linkedReceiver=-1;
if(PassingRunLink(p,q)<=0 || (q.Position.X-p.Position.X)*Direction(p.Team)<3 ||
!(q.RunUntil>Seconds || q.FootballRunUntil>Seconds && q.FootballRun is FootballRun.Behind or FootballRun.Overlap))return;
linkedPasser=p.Id;linkedReceiver=q.Id;linkedPassOrigin=p.Position;linkedPassUntil=Seconds+4;
SkillLinkPasses[p.Team]++;
}
void RecordLinkedReception(Footballer p,int passer)
{
if(p.Id==linkedReceiver && passer==linkedPasser && Seconds<linkedPassUntil)
{
SkillLinkReceptions[p.Team]++;
SkillLinkProgress[p.Team]+=Math.Max(0,(p.Position.X-linkedPassOrigin.X)*Direction(p.Team));
}
linkedPasser=linkedReceiver=-1;
}
void ApplyCombinationReceptionTiming(Footballer p)
{
var play=combinations[p.Team];
if(play.Stage!=1 || play.Link!=p.Id || Seconds>=play.Until || !play.Linked)return;
// The first reception used to overwrite the timing set at release.
// Facing, control and shot selection still run normally.
// At .07 s the receiver had not held the ball for the .12 s required by
// the return-pass gate. That wasted a decision and reset the slow cadence.
p.Decision=.14f;
p.ReceiveUntil=Math.Min(p.ReceiveUntil,Seconds+.12f);
p.NextDribbleTouch=Math.Min(p.NextDribbleTouch,Seconds+.06f);
BallVelocity=p.Velocity*.25f;
}
bool LinkedReceptionFacing(Footballer p,out Point look)
{
look=new();
// Officials share the same facing animation but are not either team.
if(p.Team<0 || p.Team>=combinations.Length || p.Id<0 || p.Id>=linkedRunUntil.Length)return false;
var play=combinations[p.Team];
if(Owner>=0 || PassReceiver!=p.Id || play.Stage!=1 || !play.Linked || play.Link!=p.Id ||
Seconds>=play.Until || Passer!=play.Starter || (Ball-p.Position).Length>12)return false;
// Open the body while the first pass travels. Watching only the incoming
// ball left the receiver needing a full turn before the return pass.
var outlet=play.ThirdLinked && play.Third>=0?play.ThirdTarget:play.StarterTarget;
look=(Ball-p.Position).Unit*.45f+(outlet-p.Position).Unit*.70f;
return look.Length>.1f;
}
}