Partial MatchSimulation class for a soccer match. Handles the period clock, end-of-half whistle, players and referee walking to the bench, halftime transition, and the final whistle or shootout trigger.
using System;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
// Match time and the presentation walk are separate: the clock stops at the whistle.
bool managedMatchClock, finalWhistleComplete;
public float PeriodWalkRemaining { get; private set; }
public int SecondHalfIntroSequence { get; private set; }
public Point[] HalfTimeReturnPositions {get;private set;}=Array.Empty<Point>();
public bool PeriodWhistleWasFinal { get; private set; }
float PeriodDeadline => SecondHalf
? 360+Math.Max(0,FirstHalfAddedMinutes)*4+Math.Max(0,SecondHalfAddedMinutes)*4
: 180+Math.Max(0,FirstHalfAddedMinutes)*4;
bool PromisingAttack()
{
if(GoalHoldRemaining>0 || ShotMissFlightRemaining>0 || IncidentRemaining>0)return false;
if(RestartRemaining>0)
{
if(SetPiece==RestartKind.Penalty)return true;
if(SetPiece is RestartKind.FreeKick or RestartKind.Indirect)
return (World(restartPoint,restartTeam)-new Point(105,34)).Length<32;
return false;
}
if(Ball.X<0 || Ball.X>105 || Ball.Y<0 || Ball.Y>68)return false;
if(shot || WindupPlayer>=0)return true;
int team=Owner>=0?Players[Owner].Team:LastTouch;
var local=World(Ball,team);
return local.X>74 && Math.Abs(local.Y-34)<29 &&
(Owner>=0 || Point.Dot(BallVelocity,new Point(Direction(team),0))>2);
}
bool CheckPeriodEnd()
{
if(MedicalActive)return false;
if(PeriodWalkRemaining>0 || PeriodDeadline>Seconds)return false;
// Added time is a minimum. Allow a short attack to conclude; a penalty
// at time is completed even if its preparation takes longer.
float extension=RestartRemaining>0 && SetPiece==RestartKind.Penalty?30:12;
if(Seconds<PeriodDeadline+extension && PromisingAttack())return false;
PeriodWhistleWasFinal=SecondHalf;
PeriodWalkRemaining=3.4f;
Owner=-1;BallVelocity=new();CancelWindup();
AddEvent(SecondHalf?"Финальный свисток":"Свисток на перерыв");
return true;
}
void AdvancePeriodWalk(float dt)
{
PeriodWalkRemaining=Math.Max(0,PeriodWalkRemaining-dt);
foreach(var p in ActivePlayers)
{
if(p.IsAcrobaticCommitted(Seconds))
{p.Velocity=new Point();p.Action="Поднимается после акробатического удара";continue;}
float bench=p.Team==0?39.5f:65.5f;
var target=new Point(bench+(p.Number-6)*.7f,1.2f+(p.Number%3)*.65f);
var delta=target-p.Position;
float speed=Math.Min(2.5f,delta.Length*1.4f);
var desired=delta.Length>.05f?delta.Unit*speed:new Point();
var change=desired-p.Velocity;
p.Velocity+=change.Unit*Math.Min(change.Length,4.5f*dt);
p.Position=Clamp(p.Position+p.Velocity*dt);
if(delta.Length>.05f)UpdateFacing(p,target,dt);
p.Action=PeriodWhistleWasFinal?"Идёт к скамейке после матча":"Идёт к скамейке на перерыв";
}
var refereeTarget=new Point(52.5f,1.4f);
var refereeDelta=refereeTarget-RefereePosition;
var previous=RefereePosition;
RefereePosition+=refereeDelta.Unit*Math.Min(refereeDelta.Length,2.2f*dt);
RefereeActor.VisualBefore=previous;RefereeActor.Position=RefereePosition;
AdvanceGait(RefereeActor,dt);
if(PeriodWalkRemaining>0)return;
if(PeriodWhistleWasFinal){if(!BeginShootout())finalWhistleComplete=true;return;}
SecondHalf=true;HalfStoppageSeconds=0;
HalfTimeReturnPositions=Players.Select(p=>p.Position).ToArray();
foreach(var p in ActivePlayers){p.Stamina=Math.Min(1,p.Stamina+.16f);p.BurstCharge=1;}
Restart(1,new Point(52.5f,34),"Второй тайм · смена ворот",true);
RefereePosition=new Point(52.5f,34);
RefereeActor.Position=RefereePosition;
SecondHalfIntroSequence++;
}
}