Partial MatchSimulation covering set-piece restarts in the football sim: picks the taker, decides corner and free-kick plans (short, near/far cross, shot), positions the wall and run-up, and releases the ball for corners, free kicks, penalties, throw-ins and goal kicks. Also holds the per-team counters for corners, free-kick outcomes and wall/duel events.
using System;
using System.Linq;
namespace Socer;
public enum RestartKind { KickOff, GoalKick, ThrowIn, Corner, FreeKick, Indirect, Penalty, DropBall }
public enum PreparedRestart { Short, Shot, NearCross, FarCross }
public sealed partial class MatchSimulation
{
public RestartKind SetPiece;
public int SetPieceTaker=-1;
public int[] Corners=new int[2], CornerDeliveries=new int[2];
public int ShortCorners;
public int[] FreeKickShots=new int[2],FreeKickCrosses=new int[2],FreeKickShort=new int[2];
public int[] CornerNear=new int[2],CornerFar=new int[2],CornerShort=new int[2];
public PreparedRestart SetPiecePlan {get;private set;}
Point setPieceZone;
bool preparedFreeKickShot;
public int WallPreparations, CornerBoxDuels;
bool keeperWallCalled;
bool indirectFlight;
bool restartKickPending;
// The spray is a referee's aid for a set wall, not a marker for every restart.
// Use attacking coordinates so both teams and halves share the same decision.
public bool DangerousFreeKick
{
get
{
if(SetPiece is not (RestartKind.FreeKick or RestartKind.Indirect))return false;
var goal=new Point(105,34)-World(restartPoint,restartTeam);
return goal.Length<=32 && goal.X>3 && Math.Abs(goal.Y)<=goal.X*1.25f;
}
}
public int SetPieceWallCount
{
get
{
if(SetPiece is not (RestartKind.FreeKick or RestartKind.Indirect))return 0;
var goal=new Point(105,34)-World(restartPoint,restartTeam);
if(goal.X<=3 || goal.Length>=44)return 0;
if(Math.Abs(goal.Y)>goal.X*1.25f)return goal.Length<37?2:1;
return goal.Length<27?5:goal.Length<37?4:3;
}
}
public bool NeedsFreeKickSpray
{
get
{
if(!DangerousFreeKick)return false;
int count=0,slots=SetPieceWallCount;
foreach(var p in Players)
if(p.InPlay && p.Team!=restartTeam && !p.Keeper && p.Number>=2 && p.Number<2+slots && ++count>=3)return true;
return false;
}
}
public Point SetPieceRunupStart
{
get
{
if(SetPiece==RestartKind.Corner)
return Clamp(restartPoint-new Point(Direction(restartTeam)*2.25f,0));
var goal=World(new Point(105,34),restartTeam);
var forward=(goal-restartPoint).Unit;
var side=new Point(-forward.Y,forward.X);
float length=SetPiece==RestartKind.Penalty?3.1f:2.25f;
return Clamp(restartPoint-forward*length+side*.48f);
}
}
Footballer ChooseTaker()
{
var team=ActivePlayers.Where(p=>p.Team==restartTeam);
if(goalKickRestart) return team.First(p=>p.Keeper);
if(SetPiece==RestartKind.Corner) return team.Where(p=>!p.Keeper).OrderByDescending(p=>
p.Profile.Crossing*.65f+p.Profile.Curve*.20f+p.Profile.Passing*.15f-(p.Position-restartPoint).Length*.006f).First();
if(SetPiece==RestartKind.Penalty) return team.Where(p=>!p.Keeper).OrderByDescending(p=>p.Profile.Finishing).First();
if(SetPiece is RestartKind.FreeKick or RestartKind.Indirect)
{
float distance=(World(new Point(105,34),restartTeam)-restartPoint).Length;
return team.Where(p=>!p.Keeper).OrderByDescending(p=>distance<34 && SetPiece==RestartKind.FreeKick?
p.Profile.FreeKickAccuracy*.65f+p.Profile.Curve*.20f+p.Profile.ShotPower*.15f:
p.Profile.Crossing*.5f+p.Profile.Passing*.35f+p.Profile.Curve*.15f).First();
}
return team.Where(p=>!p.Keeper).OrderBy(p=>(p.Position-restartPoint).Length).First();
}
void PrepareSetPiece()
{
preparedFreeKickShot=false;SetPiecePlan=PreparedRestart.Short;
if(SetPiece is not (RestartKind.Corner or RestartKind.FreeKick or RestartKind.Indirect))return;
var taker=Players[SetPieceTaker];var local=World(restartPoint,restartTeam);
float distance=(new Point(105,34)-local).Length;
float angle=Math.Abs(local.Y-34)/Math.Max(5,105-local.X);
if(SetPiece==RestartKind.Corner)
SetPiecePlan=Next()<.13f?PreparedRestart.Short:Next()<.52f?PreparedRestart.NearCross:PreparedRestart.FarCross;
else
{
float shotChance=Math.Clamp(.36f+taker.Profile.FreeKickAccuracy*.47f-
Math.Max(0,distance-24)*.025f-angle*.32f,.08f,.83f);
SetPiecePlan=SetPiece==RestartKind.FreeKick && distance<35 && angle<1.45f && Next()<shotChance
?PreparedRestart.Shot:distance<53 && local.X>47
?Next()<.55f?PreparedRestart.NearCross:PreparedRestart.FarCross:PreparedRestart.Short;
}
float near=local.Y<34?31.2f:36.8f;
float far=local.Y<34?37.0f:31.0f;
setPieceZone=World(new Point(SetPiece==RestartKind.Corner?96:94,
SetPiecePlan==PreparedRestart.NearCross?near:far),restartTeam);
}
void ArrangeSetPiece(float dt)
{
if(SetPiece==RestartKind.GoalKick)
{
var keeper=Players[SetPieceTaker];
var target=RestartRemaining>.82f?SetPieceRunupStart:restartPoint;
var delta=target-keeper.Position;var motion=delta.Unit*Math.Min(delta.Length,dt*(RestartRemaining<=.82f?5.2f:8f));
keeper.Position=Clamp(keeper.Position+motion);keeper.Velocity=dt>0?motion*(1/dt):new Point();
keeper.Action=RestartRemaining<=.82f?"Разбегается к мячу":"Готовится к удару от ворот";
UpdateFacing(keeper,restartPoint,dt);
return;
}
if(SetPiece is not (RestartKind.Corner or RestartKind.FreeKick or RestartKind.Indirect or RestartKind.Penalty)) return;
if(!Players[SetPieceTaker].InPlay) SetPieceTaker=ChooseTaker().Id;
int wallCount=SetPieceWallCount;
var taker=Players[SetPieceTaker];var attack=ActivePlayers.Where(p=>p.Team==restartTeam && !p.Keeper && p.Id!=taker.Id).OrderByDescending(p=>p.Home.X).ToArray();
foreach(var p in ActivePlayers)
{
if(Seconds<p.FallUntil || Seconds<p.ImpactUntil)continue;
var target=p.Position;
bool wall=false;
bool organizing=p.Keeper && p.Team!=restartTeam && wallCount>0;
bool heldForManual=InteractiveSetPieces && !manualRestartResolved && restartTeam==0 && DangerousManualRestart;
if(p.Id==taker.Id)
target=RestartRemaining>.82f || heldForManual?SetPieceRunupStart:restartPoint;
else if(SetPiece==RestartKind.Corner)
{
if(p.Team==restartTeam)
{
int i=Array.IndexOf(attack,p);
var zone=World(setPieceZone,restartTeam);
if(i==0) target=SetPiecePlan==PreparedRestart.Short?
World(new Point(96,World(restartPoint,restartTeam).Y<34?8:60),restartTeam):
World(new Point(92,zone.Y),restartTeam);
else if(i is >=1 and <=6) target=World(new Point(i%3==0?95:91,
Math.Clamp(zone.Y+(i-3)*2.5f,19,49)),restartTeam);
else if(!p.Keeper) target=World(new Point(Math.Min(70,p.Home.X+25),p.Home.Y),restartTeam);
}
else if(p.Keeper) target=World(new Point(2,34),p.Team);
else
{
if(attack.Length>0)
{
int i=(p.Number-2)%Math.Min(attack.Length,6);
var mark=attack[i];
target=mark.Position+new Point(-Direction(restartTeam)*.45f,(p.Number%2==0?.34f:-.34f));
}
else target=World(new Point(9,30+(p.Number%3)*4),p.Team);
if(p.Number>8)target=World(new Point(79+(p.Number%3)*3,22+(p.Number-8)*10),restartTeam);
}
}
else if(SetPiece==RestartKind.Penalty)
{
target=p.Team!=restartTeam && p.Keeper?World(new Point(.7f,34),p.Team):World(new Point(82-(p.Number%3)*2,8+p.Number*4),restartTeam);
}
else if(p.Team==restartTeam && !p.Keeper && p.Id!=taker.Id &&
(SetPiece is RestartKind.FreeKick or RestartKind.Indirect) &&
(SetPiecePlan is PreparedRestart.NearCross or PreparedRestart.FarCross))
{
int i=Array.IndexOf(attack,p);
if(i is >=0 and <3)
{
var zone=World(setPieceZone,restartTeam);
target=World(new Point(i==0?91:i==1?95:88,
Math.Clamp(zone.Y+(i-1)*3,19,49)),restartTeam);
}
else if(i==3)target=World(new Point(81,34),restartTeam);
}
else if(p.Team!=restartTeam && p.Keeper)
{
var local=World(restartPoint,p.Team);
target=World(new Point(1.1f,Math.Clamp(34+(local.Y-34)*.1f,32,36)),p.Team);
if(!keeperWallCalled && organizing)
{keeperWallCalled=true;KeeperWallCalls++;Say(p,"Стенка! Линию держать!",true);}
}
else if(p.Team!=restartTeam && !p.Keeper)
{
var goal=World(new Point(105,34),restartTeam);var forward=(goal-restartPoint).Unit;
float distance=(goal-restartPoint).Length;
int slot=p.Number-2;
if(slot>=0 && slot<wallCount)
{
target=restartPoint+forward*9.65f+new Point(-forward.Y,forward.X)*((slot-(wallCount-1)*.5f)*.76f);
wall=true;WallPreparations++;
}
else if(distance<42 && p.Number<=8)
target=goal+(restartPoint-goal).Unit*(4+(p.Number%3)*3)+new Point(0,(p.Number%2==0?1:-1)*3);
}
if(p.Team!=restartTeam && (target-restartPoint).Length<9.15f && SetPiece!=RestartKind.Penalty)
{
var away=(target-restartPoint).Unit;if(away.Length<.01f) away=new Point(-Direction(restartTeam),0);
target=restartPoint+away*9.2f;
}
target=Clamp(target);var d=target-p.Position;
float pace=p.Id==taker.Id && RestartRemaining<=.82f && !heldForManual
?manualRestartResolved?8f:5.2f:wall?12.5f:8f;
var motion=d.Unit*Math.Min(d.Length,dt*pace);
p.Position=Clamp(p.Position+motion);p.Velocity=dt>0?motion*(1/dt):new Point();
bool jostling=SetPiece==RestartKind.Corner && !p.Keeper && p.Id!=taker.Id &&
World(p.Position,restartTeam).X>84 &&
ActivePlayers.Any(q=>q.Team!=p.Team && !q.Keeper && (q.Position-p.Position).Length<1.55f);
if(jostling && p.Team!=restartTeam)CornerBoxDuels++;
p.Action=wall?"Стенка":organizing?"Руководит стенкой":p.Id==taker.Id && RestartRemaining<=.82f?"Разбегается к мячу":jostling?"Борется за позицию":"Готовится к стандарту";
UpdateFacing(p,restartPoint,dt);
}
}
void ReleaseSetPiece(Footballer taker)
{
nextKickExempt=SetPiece is RestartKind.Corner or RestartKind.ThrowIn or RestartKind.GoalKick or RestartKind.DropBall;
restartKickPending=SetPiece is RestartKind.ThrowIn or RestartKind.Indirect;
if(ReleaseManualSetPiece(taker))return;
if(SetPiece==RestartKind.Corner)
{
var receivers=ActivePlayers.Where(p=>p.Team==taker.Team && !p.Keeper && p.Id!=taker.Id).OrderBy(p=>(p.Position-restartPoint).Length).ToArray();
if(receivers.Length==0)
{
// An isolated training duel can finish with only the corner taker
// and goalkeeper on this side. Keep the restart valid without a
// phantom teammate or a pass to an inactive player.
Kick(taker,setPieceZone,22,false);
ShapeFlight(setPieceZone,StrikeKind.Cross,1.8f);
PassReceiver=-1;IntendedPassTarget=setPieceZone;
CornerDeliveries[taker.Team]++;
if(SetPiecePlan==PreparedRestart.NearCross)CornerNear[taker.Team]++;else CornerFar[taker.Team]++;
Crosses++;
Emit("corner",taker,"ПОДАЧА УГЛОВОГО",2);
return;
}
bool shortPass=SetPiecePlan==PreparedRestart.Short;
var box=receivers.Where(p=>World(p.Position,taker.Team).X>84 && Math.Abs(World(p.Position,taker.Team).Y-34)<18).ToArray();
Footballer receiver=shortPass?receivers[0]:box.Length>0?
box.OrderBy(p=>(p.Position-setPieceZone).Length).First():
receivers.OrderBy(p=>(p.Position-setPieceZone).Length).First();
var target=shortPass?receiver.Position+receiver.Velocity*.2f:setPieceZone;
Kick(taker,target,shortPass?15:22,false);
PassReceiver=receiver.Id;IntendedPassTarget=target;
if(!shortPass) {ShapeFlight(target,StrikeKind.Cross,1.8f);Crosses++;} else ShortCorners++;
CornerDeliveries[taker.Team]++;Passes[taker.Team]++;
if(shortPass)CornerShort[taker.Team]++;
else if(SetPiecePlan==PreparedRestart.NearCross)CornerNear[taker.Team]++;
else CornerFar[taker.Team]++;
Say(taker,shortPass?"Коротко!":SetPiecePlan==PreparedRestart.NearCross?"На ближнюю!":"На дальнюю!",true);
Emit("corner",taker,shortPass?"РОЗЫГРЫШ УГЛОВОГО":"ПОДАЧА УГЛОВОГО",2);
AddEvent($"{TeamName(taker.Team)} · угловой: {(shortPass?"короткий пас":SetPiecePlan==PreparedRestart.NearCross?"навес на ближнюю":"навес на дальнюю")}");return;
}
if(SetPiece==RestartKind.Penalty || SetPiece==RestartKind.FreeKick && SetPiecePlan==PreparedRestart.Shot)
{
BeginShot(taker,(World(new Point(105,34),taker.Team)-taker.Position).Length);
if(SetPiece==RestartKind.Penalty) {penaltyShot=true;Strike=StrikeKind.Driven;PowerShot=false;ShotTarget=World(new Point(106,34+(Next()-.5f)*6),taker.Team);}
else
{
preparedFreeKickShot=true;FreeKickShots[taker.Team]++;
var keeper=ActivePlayers.First(p=>p.Team!=taker.Team && p.Keeper);
float keeperY=World(keeper.Position,taker.Team).Y;
ShotTarget=World(new Point(106,keeperY<34?36.0f:32.0f),taker.Team);
Strike=taker.Profile.Curve>.58f?StrikeKind.Curl:StrikeKind.Driven;PowerShot=false;
taker.Action="Готовит удар со штрафного";
AddEvent($"{TeamName(taker.Team)} · подготовленный удар со штрафного #{taker.Number}");
}
return;
}
if(SetPiece is RestartKind.FreeKick or RestartKind.Indirect &&
SetPiecePlan is PreparedRestart.NearCross or PreparedRestart.FarCross)
{
var receiver=ActivePlayers.Where(p=>p.Team==taker.Team && p.Id!=taker.Id && !p.Keeper &&
World(p.Position,taker.Team).X>80 && Math.Abs(World(p.Position,taker.Team).Y-34)<19)
.OrderBy(p=>(p.Position-setPieceZone).Length-
p.Profile.HeadingAccuracy*3-p.Profile.Jumping*2-p.Profile.OffBall).FirstOrDefault();
if(receiver!=null)
{
var target=setPieceZone+new Point(0,(Next()-.5f)*(1-taker.Profile.Crossing)*3);
Kick(taker,target,20,false);ShapeFlight(target,StrikeKind.Cross,1.85f);
PassReceiver=receiver.Id;IntendedPassTarget=target;
Passes[taker.Team]++;Crosses++;LoftedPasses++;
FreeKickCrosses[taker.Team]++;
Say(taker,SetPiecePlan==PreparedRestart.NearCross?"На ближнюю!":"На дальнюю!",true);
Emit("cross",taker,"НАВЕС СО СТАНДАРТА",1.8f);
AddEvent($"{TeamName(taker.Team)} · навес со штрафного на {(SetPiecePlan==PreparedRestart.NearCross?"ближнюю":"дальнюю")} зону #{taker.Number}");
return;
}
}
if(SetPiece is RestartKind.ThrowIn or RestartKind.FreeKick or RestartKind.Indirect)
{
var receiver=ActivePlayers.Where(p=>p.Team==taker.Team && p.Id!=taker.Id && !p.Keeper)
.OrderBy(p=>(p.Position-taker.Position).Length).FirstOrDefault() ??
ActivePlayers.Where(p=>p.Team==taker.Team && p.Id!=taker.Id)
.OrderBy(p=>(p.Position-taker.Position).Length).FirstOrDefault();
if(receiver==null)return;
Kick(taker,receiver.Position,16,false);Passes[taker.Team]++;
if(SetPiece is RestartKind.FreeKick or RestartKind.Indirect)FreeKickShort[taker.Team]++;
PassReceiver=receiver.Id;IntendedPassTarget=receiver.Position;
if(SetPiece==RestartKind.ThrowIn) {BallHeight=1.6f;VerticalSpeed=1;}
}
}
}