Part of the soccer match simulation. Handles player perception (who can see whom, remembered positions), first-touch and heavy-touch outcomes, defender duels and challenges, off-ball run planning, shot setup and keeper parry decisions.
using System;
using System.Linq;
namespace Socer;
public enum FootballRun { None, Behind, CheckShort, Decoy, Overlap, BetweenLines }
// Football decisions consume observed positions. Physical contact/rules still use the real world.
public sealed partial class MatchSimulation
{
readonly Point[,] seenPositions=new Point[22,22];
readonly float[,] seenTimes=new float[22,22];
readonly bool[,] seen=new bool[22,22];
float nextThinking;
int lastPossessionTeam=-1;
readonly float[] counterPressUntil=new float[2];
public int PreparedReceipts,HeavyTouches,ShotAdjustments,WeakFootShots,RushedShots,ContainedDuels,ShieldActions,IntelligentRuns,CounterPresses,BoxRuns,SafeParries;
public int NearPostRuns,FarPostRuns,CutbackRuns;
public int[] RunTypes=new int[6];
public float CounterPressRemaining(int team)=>Math.Max(0,counterPressUntil[team]-Seconds);
bool CanObserve(Footballer p,Footballer q)
{
var d=q.Position-p.Position;
if(d.Length<5)return true;
if(d.Length>16+p.Profile.Vision*30)return false;
return Seconds-p.LastScanAt<.45f || Point.Dot(new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing)),d.Unit)>-.05f-p.Profile.Vision*.5f;
}
Point? Observed(Footballer p,Footballer q)
{
if(CanObserve(p,q))return q.Position;
return seen[p.Id,q.Id] && Seconds-seenTimes[p.Id,q.Id]<1.4f?seenPositions[p.Id,q.Id]:null;
}
float PerceivedPressure(Footballer p)=>ActivePlayers.Where(q=>q.Team!=p.Team)
.Select(q=>Observed(p,q)).Where(q=>q.HasValue).Select(q=>(q.Value-p.Position).Length).DefaultIfEmpty(30).Min();
// A pursuer behind the carrier is a direct threat only if he can reach
// challenge range before the carrier's next stride.
float CarrierPressure(Footballer p)
{
var goalward=(World(new Point(105,34),p.Team)-p.Position).Unit;
var forward=p.Velocity.Length>1.2f && Point.Dot(p.Velocity.Unit,goalward)>.25f?p.Velocity.Unit:goalward;
float pressure=30;
foreach(var rival in ActivePlayers.Where(q=>q.Team!=p.Team))
{
var seen=Observed(p,rival);if(!seen.HasValue)continue;
var offset=seen.Value-p.Position;
float distance=offset.Length;
if(Point.Dot(offset,forward)<-.55f && distance>1.1f)
{
float closing=Point.Dot(rival.Velocity-p.Velocity,forward);
float timeToChallenge=(distance-1.1f)/Math.Max(.1f,closing);
if(timeToChallenge>.45f)distance+=4;
}
pressure=Math.Min(pressure,distance);
}
return pressure;
}
float PerceivedSpace(Footballer p,Point at)=>ActivePlayers.Where(q=>q.Team!=p.Team)
.Select(q=>Observed(p,q)).Where(q=>q.HasValue).Select(q=>(q.Value-at).Length).DefaultIfEmpty(12).Min();
float PerceivedLane(Footballer p,Point at,bool shotLane=false)=>ActivePlayers.Where(q=>q.Team!=p.Team && (!shotLane || !q.Keeper))
.Select(q=>Observed(p,q)).Where(q=>q.HasValue).Select(q=>DistanceToSegment(q.Value,p.Position,at)).DefaultIfEmpty(8).Min();
void UpdateFootballThinking(float dt)
{
if(Owner>=0)
{
int team=Players[Owner].Team;
if(lastPossessionTeam>=0 && team!=lastPossessionTeam)
{
int lost=lastPossessionTeam;
if(TacticalCharacters[team]==TacticalCharacter.Counter)
{counterAttackUntil[team]=Seconds+6;CounterAttacks[team]++;}
PossessionLosses[lost]++;
ResetCombinations();
bool press=Tactics[lost]==2 || Approach[lost]==3 || (lost==0 && CoachOrder==1 && Seconds<CoachUntil);
counterPressUntil[lost]=press?Seconds+3.2f:0;
if(press)CounterPresses++;
foreach(var p in ActivePlayers){p.FootballRun=FootballRun.None;p.BoxRole=0;p.ShotSetupUntil=0;p.ShieldUntil=0;}
}
lastPossessionTeam=team;
}
foreach(var p in ActivePlayers)
{
p.Confidence+=(.6f-p.Confidence)*dt*.012f;
if(p.Id!=Owner){p.ShotSetupUntil=0;p.PlanUntil=0;}
}
if(Seconds<nextThinking)return;
nextThinking=Seconds+.22f;
UpdatePassCalls();
foreach(var p in ActivePlayers)
{
ScanPitch(p);
foreach(var q in ActivePlayers)
if(q.Team!=p.Team && CanObserve(p,q)){seen[p.Id,q.Id]=true;seenPositions[p.Id,q.Id]=q.Position;seenTimes[p.Id,q.Id]=Seconds;}
if(!p.Keeper && Owner<0 && LastTouch==p.Team && (Ball-p.Position).Length<28 && BallSpeed>3)
{
bool receiving=PassReceiver==p.Id || (BallVelocity.Length>0 && DistanceToSegment(p.Position,Ball,Ball+BallVelocity*.65f)<2.2f);
if(receiving && Seconds>=p.ReceiveReadyUntil)
{p.ReceiveReadyUntil=Seconds+1.1f;p.ReceiveDirection=ChooseFirstTouch(p);PreparedReceipts++;p.Action="Открывает корпус под пас";}
}
if(Owner>=0 && p.Team==Players[Owner].Team && p.Id!=Owner && !p.Keeper && Seconds>=p.FootballRunUntil)
PlanFootballRun(p,Players[Owner]);
else if(Owner>=0 && p.Team==Players[Owner].Team && p.Id!=Owner && !p.Keeper &&
linkedRunPasser[p.Id]!=Owner && p.RunUntil<=Seconds && p.BoxRole==0 &&
p.FootballRun is not (FootballRun.Behind or FootballRun.Overlap))
// A new linked playmaker can trigger a safe opening immediately,
// rather than waiting for an unrelated short-check route to expire.
TryPlanLinkedRun(p,Players[Owner]);
}
}
Point ChooseFirstTouch(Footballer p)
{
var forward=new Point(Direction(p.Team),0);
Point best=forward;float value=float.MinValue;
foreach(var option in new[]{forward,(forward+new Point(0,-.7f)).Unit,(forward+new Point(0,.7f)).Unit,new Point(0,-1),new Point(0,1),forward*-1,p.Velocity.Length>1?p.Velocity.Unit:forward})
{
var at=p.Position+option*2.5f;
float score=PerceivedSpace(p,at)+Point.Dot(option,forward)*1.6f+Point.Dot(option,p.Velocity.Unit)*1.3f-Math.Max(0,1.5f-CarryLane(p,at))*8;
if(score>value){value=score;best=option;}
}
return best;
}
bool FirstTouch(Footballer p,int passer,float speed,Point incoming)
{
if(p.Keeper || passer<0 || Players[passer].Team!=p.Team || speed<3 || BallHeight>.9f)return false;
float pressure=PerceivedPressure(p);
float prepared=Seconds<p.ReceiveReadyUntil?.13f:0;
var combination=combinations[p.Team];
bool linkedReturn=combination.Linked && combination.Stage==1 && combination.Link==p.Id &&
combination.Starter==passer && Seconds<combination.Until;
if(linkedReturn)prepared=Math.Max(prepared,.18f);
if(passer>=0 && PassingRunLink(Players[passer],p)>0 &&
(p.RunUntil>Seconds || p.FootballRunUntil>Seconds && p.FootballRun is FootballRun.Behind or FootballRun.Overlap))
prepared=Math.Max(prepared,.18f);
float firstTouch=SkillStrength(p,UniqueSkill.FirstTouch);
float quality=Math.Min(1,p.Skill*.45f+p.Profile.Composure*.25f+p.Stamina*.17f+prepared+.10f*firstTouch);
float difficulty=Math.Clamp((speed-16)/38,0,.4f)+Math.Clamp((5-pressure)/5,0,1)*.30f;
bool shortDelivery=(Players[passer].Position-p.Position).Length<16 && speed<20;
if(shortDelivery && pressure>2.5f)difficulty*=.35f+(1-quality)*.4f;
p.ReceiveDirection=ChooseFirstTouch(p);p.ReceiveUntil=Seconds+(.3f+(1-quality)*.25f)*(1-.17f*firstTouch);
p.ReceiveReadyUntil=0;
if(difficulty>.12f && Next()<Math.Clamp(difficulty-quality*.20f,.01f,.30f))
{
// A heavy touch remains a physically contestable ball, not a scripted turnover.
Owner=-1;Passer=-1;LastTouch=p.Team;shot=false;ResetFlight();
float severity=Next();
var lateral=new Point(-incoming.Unit.Y,incoming.Unit.X);
Ball=p.ControlPoint;BallVelocity=(p.ReceiveDirection*.35f+incoming.Unit*.5f+lateral*((Next()-.5f)*1.1f)).Unit*(1.2f+severity*(3.5f+(1-quality)*2));
VerticalSpeed=.35f+severity*1.2f;
p.RecoveryChaseUntil=Seconds+2.8f;PassReceiver=p.Id;
PickupDelay=.1f;p.Recovery=.12f;p.Confidence=Math.Max(.15f,p.Confidence-.06f);HeavyTouches++;
p.Action=severity>.55f?"Далеко отпустил при приёме":"Мяч отскочил от ноги";Say(p,"Неудобно!",false);return true;
}
p.Confidence=Math.Min(.95f,p.Confidence+.012f);
p.ReceiveOnMove=quality>.55f && pressure>2 && PerceivedSpace(p,p.Position+p.ReceiveDirection*2)>2.5f;
if(p.ReceiveOnMove)p.ReceiveUntil=Seconds+.10f+(1-quality)*.12f;
if(linkedReturn)p.ReceiveOnMove=false;
return false;
}
bool ShouldChallenge(Footballer defender,Footballer carrier)
{
if(defender.Keeper || Seconds<defender.FallUntil || Seconds<carrier.FallUntil)return false;
float reach=(defender.Position-Ball).Length;
if(CanBodyChallenge(defender,carrier))return true;
if(reach<.70f+defender.Profile.StandingTackle*.43f)return true;
if(ShouldSlide(defender,carrier))return true;
// A normal dribble touch is not automatically a tackle opportunity.
bool exposed=(Ball-carrier.Position).Length>.92f-defender.Profile.Defence*.48f || Seconds<carrier.ReceiveUntil;
bool behind=Point.Dot((defender.Position-carrier.Position).Unit,
carrier.Velocity.Length>1?carrier.Velocity.Unit:new Point(MathF.Cos(carrier.Facing),MathF.Sin(carrier.Facing)))<-.45f;
return !behind && reach<.91f+defender.Profile.StandingTackle*.49f && (exposed || WindupPlayer==carrier.Id ||
(carrier.Velocity.Length<1 && Seconds>=defender.ContainUntil));
}
Point DuelMovement(Footballer p,Footballer carrier,Point target)
{
float distance=(carrier.Position-p.Position).Length;
if(p.Id==chasers[p.Team] && distance<18)
{
var goal=World(new Point(0,34),p.Team);var goalside=(goal-carrier.Position).Unit;
var offset=p.Position-carrier.Position;
if(Point.Dot(offset,goalside)<-.4f && distance>1.1f)
{
// Catch the runner's near shoulder. A distant goal-side waypoint
// made the defender run past the duel without ever making contact.
if(p.DefensiveRecoveryUntil<=Seconds)DefensiveRecoveryRuns++;
p.DefensiveRecoveryUntil=Seconds+1.15f;
var run=carrier.Velocity.Length>1?carrier.Velocity.Unit:goalside;
var side=new Point(-run.Y,run.X);
float flank=Point.Dot(offset,side)>=0?1:-1;
if(Math.Abs(Point.Dot(offset,side))<.15f)flank=carrier.Position.Y<34?1:-1;
float lead=Math.Clamp(distance*.14f,.27f,.9f);
float anticipation=.10f+p.Profile.Defence*.14f;
p.Action="Рывок вдогонку";
p.DecisionReason="Возвращается к ближнему плечу нападающего";
return Clamp(carrier.Position+carrier.Velocity*anticipation+goalside*lead+side*flank*.68f);
}
if(CanBodyChallenge(p,carrier)) {p.Action="Сопровождает плечом";return carrier.Position+carrier.Velocity*.18f+offset.Unit*.72f;}
if((goal-carrier.Position).Length<30 && Point.Dot(offset,goalside)>.1f && distance<8)
{
p.Action="Закрывает линию удара";
p.DecisionReason="Сближается со стороны ворот · не открывает удар";
if(WindupPlayer==carrier.Id)p.BlockUntil=Seconds+.25f;
return Clamp(carrier.Position+carrier.Velocity*(.05f+p.Profile.Defence*.20f)+
goalside*(distance>2?1.25f:.85f));
}
if(ShouldChallenge(p,carrier)){p.Action="Выбирает момент отбора";return Ball+goalside*.25f;}
if(Seconds>=p.ContainUntil+.5f){p.ContainUntil=Seconds+.7f+p.Profile.Defence*.5f;ContainedDuels++;}
p.Action=distance>3?"Перекрывает путь к воротам":"Сдерживает · вытесняет на фланг";
float inside=carrier.Position.Y<34?1:-1;
return Clamp(carrier.Position+carrier.Velocity*(.05f+p.Profile.Defence*.22f)+
goalside*(distance>3?2.1f:1.25f)+new Point(0,inside*.3f));
}
var defenders=ActivePlayers.Where(q=>q.Team==p.Team && !q.Keeper && q.Id!=chasers[p.Team] && Seconds>=q.FallUntil)
.OrderBy(q=>(q.Position-carrier.Position).Length).Take(2).ToArray();
if(distance<23 && defenders.Length>0 && p.Id==defenders[0].Id)
{
p.Action="Страхует партнёра";
var goal=World(new Point(0,34),p.Team);
return carrier.Position+(goal-carrier.Position).Unit*4+new Point(0,carrier.Position.Y<34?2:-2);
}
if(distance<25 && defenders.Length>1 && p.Id==defenders[1].Id)
{
var receiver=ActivePlayers.Where(q=>q.Team==carrier.Team && q.Id!=carrier.Id && !q.Keeper && Observed(p,q).HasValue)
.OrderBy(q=>(Observed(p,q).Value-carrier.Position).Length).FirstOrDefault();
if(receiver!=null){p.Action="Закрывает линию передачи";return carrier.Position+(Observed(p,receiver).Value-carrier.Position).Unit*5;}
}
return target;
}
void PlanFootballRun(Footballer p,Footballer carrier)
{
p.FootballRun=FootballRun.None;p.FootballRunUntil=Seconds+1.4f;
if(TryPlanLinkedRun(p,carrier))return;
if(carrier.Keeper || !AttackUnit(p) || p.Stamina<.27f || (p.Position-carrier.Position).Length>32)return;
var candidates=ActivePlayers.Where(q=>q.Team==p.Team && q.Id!=Owner && AttackUnit(q) && q.Stamina>.27f)
.OrderBy(q=>(q.Position-carrier.Position).Length-q.Profile.OffBall*5).Take(2).ToArray();
int slot=Array.FindIndex(candidates,q=>q.Id==p.Id);if(slot<0)return;
int choice=(p.Id+p.RunSequence+++(Seed&7))%5;
if(TacticalCharacters[p.Team]==TacticalCharacter.Counter && counterAttackUntil[p.Team]>Seconds)
choice=(int)(slot==0?FootballRun.Behind:FootballRun.CheckShort)-1;
// A forward pocket near the box calls for penetration, not a cyclic short check.
var pocket=OnsideRunTarget(p,carrier.Position+new Point(Direction(p.Team)*13,p.Position.Y<34?-6:6));
float hunter=SkillStrength(p,UniqueSkill.SpaceHunter);
if(hunter>0 && World(carrier.Position,p.Team).X>42 && PerceivedSpace(p,pocket)>2.5f &&
IsOnside(p,carrier.Position))choice=(int)FootballRun.Behind-1;
if(slot==0 && World(carrier.Position,p.Team).X is >60 and <88 &&
PerceivedSpace(p,pocket)>3 && (pocket.X-p.Position.X)*Direction(p.Team)>3)
choice=(int)FootballRun.Behind-1;
p.FootballRun=(FootballRun)(choice+1);RunTypes[choice+1]++;IntelligentRuns++;
p.FootballRunStarted=Seconds;p.FootballRunUntil=Seconds+2.2f+p.Profile.OffBall;
float dir=Direction(p.Team),side=p.Position.Y<carrier.Position.Y?-1:1;
p.FootballRunTarget=p.FootballRun switch
{
FootballRun.Behind=>carrier.Position+new Point(dir*13,side*6),
FootballRun.CheckShort=>carrier.Position+new Point(-dir*5,side*5),
FootballRun.Decoy=>p.Position+new Point(dir*9,side*5),
FootballRun.Overlap=>carrier.Position+new Point(dir*9,carrier.Position.Y<34?-7:7),
_=>carrier.Position+new Point(dir*7,side*9)
};
if(p.FootballRun==FootballRun.BetweenLines)
{
var defenders=ActivePlayers.Where(q=>q.Team!=p.Team && !q.Keeper && Observed(p,q).HasValue)
.Select(q=>Observed(p,q).Value).OrderBy(q=>(q-p.FootballRunTarget).Length).Take(2).ToArray();
if(defenders.Length==2)p.FootballRunTarget=(defenders[0]+defenders[1])*.5f;
}
p.FootballRunTarget=OnsideRunTarget(p,p.FootballRunTarget);
if(hunter>0 && p.FootballRun==FootballRun.Behind)
{
p.FootballRunTarget=OnsideRunTarget(p,p.FootballRunTarget+new Point(Direction(p.Team)*1.8f*hunter,0));
p.FootballRunUntil+=.4f*hunter;
}
}
Point OnsideRunTarget(Footballer p,Point target)
{
int dir=Direction(p.Team);
var known=ActivePlayers.Where(q=>q.Team!=p.Team && Observed(p,q).HasValue)
.Select(q=>Observed(p,q).Value.X*dir).OrderByDescending(x=>x).ToArray();
if(known.Length>=2 && target.X*dir>Math.Max(Ball.X*dir,known[1])-1)
target.X=(Math.Max(Ball.X*dir,known[1])-1)*dir;
return Clamp(target);
}
Point FootballMovement(Footballer p,Point target,int possessionTeam)
{
if(p.Keeper || Seconds<p.FallUntil || DuelDefender==p.Id || WindupPlayer==p.Id)return target;
if(Owner==p.Id)
{
if(p.ShotSetupUntil>Seconds){p.Action="Подстраивает шаг под удар";return p.ShotSetupTarget;}
if(p.ReceiveUntil>Seconds){p.Action="Первое касание в свободную зону";return p.Position+p.ReceiveDirection*2;}
if(p.ShieldUntil>Seconds){p.Action="Прикрывает мяч корпусом";return p.Position+p.ShieldDirection*.4f;}
if(p.AttackEscapeUntil>Seconds){p.Action="Смещается для обострения";return p.AttackEscapeTarget;}
if(p.HesitateUntil>Seconds){p.Action="Ищет решение";return p.Position+p.Velocity*.1f;}
if(p.PlanUntil>Seconds && p.PlanChoice==BallChoice.Carry)return p.PlanTarget;
return target;
}
if(Owner>=0 && possessionTeam!=p.Team && !Players[Owner].Keeper)
{
if(p.BlockUntil>Seconds)return target;
target=DuelMovement(p,Players[Owner],target);
if(counterPressUntil[p.Team]>Seconds && p.Stamina>.4f && (p.Position-Ball).Length<13 && p.Id==secondChasers[p.Team])
{p.Action="Контрпрессинг после потери";return Ball+new Point(-Direction(p.Team)*1.5f,p.Position.Y<Ball.Y?-2:2);}
return target;
}
if(TryFinalThirdMovement(p,possessionTeam,out var boxTarget))return boxTarget;
p.BoxRole=0;
if(Owner>=0 && possessionTeam==p.Team && p.Home.X>30 &&
World(Ball,p.Team).X is >48 and <98 &&
Math.Abs(p.Home.Y-34)>17)
{
float x=Math.Min(94,Math.Max(World(Ball,p.Team).X+7,World(p.Position,p.Team).X+1));
float y=p.Home.Y<34?10:58;
p.Action="Открывается для навеса";
return OnsideRunTarget(p,World(new Point(x,y),p.Team));
}
if(Owner>=0 && possessionTeam==p.Team && StrikerRole(p) &&
World(Ball,p.Team).X is >43 and <77 &&
Math.Sign(p.Home.Y-34)==Math.Sign(World(Ball,p.Team).Y-34))
{
var local=World(p.Position,p.Team);
p.Action="Забегает на фланг";
return OnsideRunTarget(p,World(new Point(Math.Min(85,Math.Max(World(Ball,p.Team).X+11,local.X+2)),p.Home.Y<34?14:54),p.Team));
}
if(Owner>=0 && possessionTeam==p.Team && p.Home.X is >30 and <55 &&
Math.Abs(p.Home.Y-34)<14 &&
World(Ball,p.Team).X is >25 and <76 &&
(p.FootballRun==FootballRun.None || Seconds>=p.FootballRunUntil))
{
var ball=World(Ball,p.Team);
bool near=Math.Abs(p.Home.Y-34)<3 && p.Home.X<40 ||
Math.Sign(p.Home.Y-34)==Math.Sign(ball.Y-34) ||
Math.Abs(ball.Y-34)<3 && p.Home.Y<34;
float y=near?Math.Clamp(ball.Y+(p.Home.Y<34?-8:8),10,58):
Math.Clamp(34+(p.Home.Y<34?-5:5),18,50);
p.Action=near?"Поддерживает под пас":"Открывается между линиями";
return OnsideRunTarget(p,World(new Point(ball.X+(near?-6:8),y),p.Team));
}
if(Owner>=0 && possessionTeam==p.Team && p.FootballRun!=FootballRun.None && Seconds<p.FootballRunUntil)
{
p.Action=p.FootballRun switch{FootballRun.Behind=>"Рывок за спину",FootballRun.CheckShort=>"Отходит за передачей",FootballRun.Decoy=>"Ложное открывание",FootballRun.Overlap=>"Забегание по флангу",_=>"Открывается между линиями"};
if(p.FootballRun==FootballRun.Decoy && Seconds-p.FootballRunStarted>.8f)
return OnsideRunTarget(p,p.FootballRunTarget+new Point(-Direction(p.Team)*8,p.Position.Y<34?5:-5));
return OnsideRunTarget(p,p.FootballRunTarget);
}
return target;
}
bool SetUpShot(Footballer p,float distance,float pressure)
{
if(p.ShotSetupUntil>0)
{
if(Seconds<p.ShotSetupUntil)return true;
p.ShotSetupUntil=0;
if(FinishingWindow(p).Lane<.45f && distance>12)
{p.Decision=.12f;p.Action="Отказался от закрытого удара";return false;}
BeginShot(p,distance);return true;
}
var goalward=(new Point(Direction(p.Team)>0?105:0,34)-p.Position).Unit;
float alignment=Point.Dot(new Point(MathF.Cos(p.Facing),MathF.Sin(p.Facing)),goalward);
bool awkward=alignment<.88f || Seconds-p.LastControlAt<.65f;
if(awkward && pressure>2.5f && p.Profile.Composure>.4f)
{
p.ShotSetupUntil=Seconds+.22f+(1-p.Skill)*.25f;
p.ShotSetupTarget=Clamp(p.Position+goalward*.4f+new Point(-goalward.Y,goalward.X)*(p.StrongFoot*.14f));
p.Decision=.1f;ShotAdjustments++;p.Action="Подстраивает шаг под удар";return true;
}
BeginShot(p,distance);return true;
}
float ShotBalance(Footballer p)
{
float pressure=CarrierPressure(p);
p.KickFoot=p.StrongFoot;
var goalward=(new Point(Direction(p.Team)>0?105:0,34)-p.Position).Unit;
var side=new Point(-goalward.Y,goalward.X);
float ballSide=Point.Dot(Ball-p.Position,side);
p.UsedWeakFoot=pressure<2.5f && ballSide*p.StrongFoot<-.08f;
if(p.UsedWeakFoot){p.KickFoot=-p.StrongFoot;WeakFootShots++;}
p.RushedShot=pressure<1.7f || (Seconds-p.LastControlAt<.25f && pressure<3);
if(p.RushedShot)RushedShots++;
return Math.Clamp(1-(p.UsedWeakFoot?((1-p.Skill)*.35f+.12f)*(6-Math.Clamp(p.WeakFootRating,1,5))/3f:0)-(p.RushedShot?.22f:0)-(1-p.Stamina)*.15f, .45f,1);
}
bool KeeperShouldParry(Footballer keeper,float speed,float height)
{
if(keeper.KeeperBlockUntil>Seconds && keeper.KeeperBlockKind==2)return true;
float stretch=Math.Abs(Ball.Y-keeper.Position.Y);
float pressure=ActivePlayers.Where(q=>q.Team!=keeper.Team).Min(q=>(q.Position-keeper.Position).Length);
float handling=keeper.Profile.GkHandling*.65f+keeper.Profile.Composure*.2f+keeper.Stamina*.15f;
bool safe=SkillActive(keeper,UniqueSkill.SafeHands);
if(!safe && speed>15 && speed<33 && height<2.1f && pressure>1.4f && stretch<1.35f)
safe=ActivateSkill(keeper,UniqueSkill.SafeHands);
float grip=safe?keeper.ActiveSkillPower:0;
return speed>20+handling*10+grip*3 || speed>12+grip*4 && stretch>.95f+grip*.12f ||
keeper.DiveRemaining>0 && speed>17+grip*4 && stretch>.65f+grip*.1f ||
speed>15+grip*3 && (height>1.9f || pressure<1.3f);
}
void ResetFootballContext()
{
ResetCombinations();
lastPossessionTeam=-1;counterPressUntil[0]=counterPressUntil[1]=0;
foreach(var p in Players){p.FootballRun=FootballRun.None;p.ReceiveUntil=p.ReceiveReadyUntil=p.ShotSetupUntil=p.ShieldUntil=p.ContainUntil=0;p.BodyLeanUntil=0;p.PlanUntil=0;p.BoxRole=0;}
}
}