Partial of MatchSimulation handling interactive set pieces: penalties, corners, free kicks and other restarts where the coach picks the play. Pauses the match clock while waiting, tracks delay warnings and bookings, and turns the submitted shot/pass/cross target into ball velocity, spin and height.
using System;
using System.Linq;
namespace Socer;
public enum SetPiecePlay { Shot, GroundPass, Cross }
public sealed partial class MatchSimulation
{
public static MatchSimulation InteractiveRestartPreview(RestartKind kind,int team=0,bool secondHalf=false)
{
var m=new MatchSimulation(823){SecondHalf=secondHalf,Seconds=secondHalf?200:40,InteractiveSetPieces=true};
var spot=m.World(kind==RestartKind.Penalty?new Point(94,34):kind==RestartKind.Corner?new Point(104.3f,.7f):new Point(80,29),team);
m.Restart(team,spot,"Стандарт",kind:kind);m.ArrangeSetPiece(12);m.RestartRemaining=.2f;
return m;
}
public bool InteractiveSetPieces;
public bool ManualRestartWaiting {get;private set;}
public float ManualRestartWait {get;private set;}
public int RestartSequence {get;private set;}
public int DelayWarnings {get;private set;}
public bool ManualRestartWarned=>ManualRestartWait>=10;
public Point SetPieceSpot=>restartPoint;
public bool ManualShotAllowed=>SetPiece is RestartKind.Penalty or RestartKind.FreeKick;
public bool DangerousManualRestart
{
get
{
if(SetPiece==RestartKind.Penalty)return true;
if(SetPiece!=RestartKind.FreeKick)return false;
return DangerousFreeKick;
}
}
bool manualRestartResolved,manualCommand,manualShot;
SetPiecePlay manualPlay;
Point manualTarget;
float manualHeight,manualPower;
void ResetManualRestart()
{
RestartSequence++;ManualRestartWaiting=false;ManualRestartWait=0;
manualRestartResolved=manualCommand=manualShot=false;
}
bool WaitForManualRestart()
{
if(!InteractiveSetPieces || manualRestartResolved || restartTeam!=0 || RestartRemaining>.35f ||
!DangerousManualRestart)return false;
// The match clock pauses while the coach chooses a kick. Never freeze
// the referee in the crouched spraying pose during this thinking time.
sprayAnimationRemaining=0;RefereeSpraying=false;RefereeActor.Action="Следит за игрой";
ManualRestartWaiting=true;RestartRemaining=.35f;
var look=World(new Point(105,34),restartTeam)-restartPoint;
Players[SetPieceTaker].Facing=MathF.Atan2(look.Y,look.X);return true;
}
public void AdvanceManualRestartWait(float realDelta)
{
if(!ManualRestartWaiting)return;
float dt=Math.Clamp(realDelta,0,.1f),before=ManualRestartWait;
ManualRestartWait+=dt;
// Thinking time is compensated by the referee; it cannot consume the half.
if((SecondHalf?SecondHalfAddedMinutes:FirstHalfAddedMinutes)<0)HalfStoppageSeconds+=dt*.25f;
if(before<10 && ManualRestartWait>=10)
{
DelayWarnings++;Signal("НЕ ЗАТЯГИВАЙТЕ!",restartPoint);
AddEvent("Судья: разыгрывайте, не задерживайте матч!");
}
if(ManualRestartWait>=20)
{
if(DelayWarnings>=2 && SetPieceTaker>=0 && !Players[SetPieceTaker].SentOff)
ShowCard(Players[SetPieceTaker],false,"повторная затяжка стандарта");
AddEvent("Судья требует розыгрыш · команда выполняет стандарт");
AutoReleaseSetPiece();
}
// The football clock and roots remain frozen while the coach thinks,
// but a foul/card reaction must enter, finish and release in real time.
// Run after discipline so a delay booking is queued on the same frame.
UpdateFootballEmotions(dt);
}
public void AutoReleaseSetPiece()
{
if(!ManualRestartWaiting)return;
manualRestartResolved=true;ManualRestartWaiting=false;RestartRemaining=.35f;
}
public bool SubmitSetPiece(SetPiecePlay play,Point target,float height,float power)
{
if(!ManualRestartWaiting || restartTeam!=0 || !float.IsFinite(target.X+target.Y+height+power))return false;
if(SetPiece==RestartKind.Penalty && play!=SetPiecePlay.Shot)return false;
if(play==SetPiecePlay.Shot && !ManualShotAllowed)return false;
if(play!=SetPiecePlay.Shot && (Clamp(target)-restartPoint).Length<2)return false;
manualPlay=play;manualPower=Math.Clamp(power,.15f,1);manualHeight=Math.Clamp(height,0,2.9f);
manualTarget=play==SetPiecePlay.Shot?new Point(Direction(0)>0?105:0,Math.Clamp(target.Y,29.6f,38.4f)):Clamp(target);
manualCommand=true;AutoReleaseSetPiece();return true;
}
bool ReleaseManualSetPiece(Footballer taker)
{
if(!manualCommand)return false;
manualCommand=false;
float skill=manualPlay==SetPiecePlay.Shot?taker.Profile.Finishing:taker.Profile.Passing;
float error=(1-skill)*.7f+Math.Max(0,manualPower-.8f)*2;
var target=manualTarget+new Point(0,(Next()-.5f)*error*2);
if(manualPlay==SetPiecePlay.Shot)
{
BeginShot(taker,(target-taker.Position).Length);
ShotTarget=target;Strike=manualPower>.8f?StrikeKind.Power:StrikeKind.Driven;PowerShot=manualPower>.8f;
manualShot=true;manualHeight=Math.Max(0,manualHeight+(Next()-.5f)*error*.35f);
}
else
{
Kick(taker,target,12+manualPower*17,false);
PassReceiver=ActivePlayers.Where(p=>p.Team==taker.Team && p.Id!=taker.Id && !p.Keeper)
.OrderBy(p=>(p.Position-target).Length).Select(p=>p.Id).FirstOrDefault(-1);
IntendedPassTarget=target;Passes[taker.Team]++;
if(manualPlay==SetPiecePlay.Cross)
{
ShapeFlight(target,StrikeKind.Cross,1.5f);
float duration=Math.Clamp((target-Ball).Length/(10+manualPower*12),.75f,3.1f);
BallVelocity=(target-Ball)*(.08f/(1-MathF.Exp(-.08f*duration)));
VerticalSpeed=(1.5f-BallHeight)/duration+4.905f*duration;
float angle=-BallSpin*duration*.45f;float c=MathF.Cos(angle),s=MathF.Sin(angle);
BallVelocity=new Point(BallVelocity.X*c-BallVelocity.Y*s,BallVelocity.X*s+BallVelocity.Y*c);Crosses++;
}
if(SetPiece==RestartKind.Corner){CornerDeliveries[taker.Team]++;if(manualPlay==SetPiecePlay.GroundPass)ShortCorners++;}
AddEvent(manualPlay==SetPiecePlay.Cross?"Тренер · подача в выбранную зону":"Тренер · розыгрыш низом");
}
return true;
}
void ShapeManualShot()
{
manualShot=false;BallSpin=0;
BallVelocity=(ShotTarget-Ball).Unit*(16+manualPower*20);
float distance=(ShotTarget-Ball).Length;
float duration=-MathF.Log(Math.Max(.05f,1-.08f*distance/BallSpeed))/.08f;
BallHeight=.12f;VerticalSpeed=(manualHeight-BallHeight)/Math.Max(.05f,duration)+4.905f*duration;
}
}