Part of the Socer match simulation. Holds the attack urgency score, decides when AI players call for a pass (and when an ignored call makes them frustrated), and resolves first-time shots when a moving ball crosses within reach of an attacker.
using System;
using System.Linq;
namespace Socer;
public sealed partial class MatchSimulation
{
public int LongRangeShots,MovingShots;
// Score pressure grows late in the game; it does not change accuracy or physics.
float AttackUrgency(int team)=>Math.Max(team==0 && FieldAttackUntil>Seconds?(FieldShootUntil>Seconds?.95f:.55f):0,Goals[team]<Goals[1-team]
?Math.Clamp(.3f+Minute/90f*.55f+(Goals[1-team]-Goals[team]-1)*.12f,0,1):0);
void UpdatePassCalls()
{
foreach(var p in ActivePlayers.Where(p=>!p.Keeper))
{
if(p.IsAcrobaticCommitted(Seconds)){p.PassCallUntil=0;continue;}
if(Owner==p.Id){p.PassCallUntil=0;p.IgnoredPassCalls=0;p.PassCallOwner=-1;continue;}
if(p.PassCallOwner>=0 && Seconds>=p.PassCallUntil)
{
if(Owner>=0 && Players[Owner].Team==p.Team && PassReceiver!=p.Id)
{
p.IgnoredPassCalls++;
if(p.IgnoredPassCalls>=2)
{
p.Frustration=Math.Min(1,p.Frustration+.16f);
p.AngryPassCall=true;p.PassCallUntil=Seconds+3;
SpeakEmotion(p,p.Profile.Composure<.5f?"Да блин, я же открыт!":"Почему без паса?!");
}
}
p.PassCallOwner=-1;
}
if(Seconds<p.NextPassCallAt || Seconds<p.PassCallUntil || Owner<0 || Players[Owner].Team!=p.Team ||
Seconds<p.FallUntil || Seconds-p.LastSlideAt<2.1f || p.Stamina<.2f)continue;
var goal=World(new Point(105,34),p.Team);
if((goal-p.Position).Length>27 || (Ball-p.Position).Length<5 || (Ball-p.Position).Length>30 ||
!IsOnside(p,Ball) || PerceivedSpace(p,p.Position)<3 || PerceivedLane(p,goal,true)<1.2f)continue;
// A call is information for the coach, not omniscience for the carrier.
if(ActivePlayers.Count(q=>q.Team==p.Team && q.PassCallUntil>Seconds)>=2)continue;
p.AngryPassCall=false;p.PassCallUntil=Seconds+3;p.NextPassCallAt=Seconds+10;
p.PassCallOwner=Owner;
}
}
bool TryOpenPlayFirstTime(Point before,float beforeHeight)
{
if(RehearsingStrategy || Strategy is {Done:false} || shot || PickupDelay>0 || Passer<0 || BallSpeed<5)return false;
var segment=Ball-before;float length2=Point.Dot(segment,segment);
foreach(var p in ActivePlayers.Where(p=>p.Team==LastTouch && p.Id!=Passer && !p.Keeper && Seconds>=p.FallUntil && Seconds-p.LastSlideAt>=2.1f && !p.IsAcrobaticCommitted(Seconds)))
{
float t=length2<.0001f?0:Math.Clamp(Point.Dot(p.Position-before,segment)/length2,0,1);
var contact=before+segment*t;
if((contact-p.Position).Length>DribbleFootReach || beforeHeight+(BallHeight-beforeHeight)*t>.45f)continue;
var aim=World(new Point(105,34),p.Team)-p.Position;
if(aim.Length>24 || Point.Dot(new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing)),aim.Unit)<.65f ||
EvaluateBallDecision(p,CarrierPressure(p)).Choice!=BallChoice.Shoot || Next()>.3f+p.Profile.Finishing*.45f)continue;
if(CheckOffside(p))return true;
float incoming=BallSpeed;Completed[p.Team]++;ClearOffside();
Ball=contact;BallHeight=0;Owner=p.Id;
BeginShot(p,aim.Length);
float contactTechnique=p.Profile.Volleys*.6f+p.Profile.Finishing*.25f+p.Skill*.15f;
float contactSpread=.25f+(1-contactTechnique)*(2+incoming*.12f)+incoming*.015f+
(1-p.Profile.Composure)*.35f;
contactSpread*=1-.22f*SkillStrength(p,UniqueSkill.VolleyMaster);
ShotTarget.Y+=(Next()-.5f)*contactSpread;
WindupPlayer=-1;ReleaseShot(p);FirstTimeShots++;p.Action="Удар с ходу";
return true;
}
return false;
}
}