CoachCommands.cs

Partial MatchSimulation class that adds a coach command. Three orders (press, attack, conserve) change player target positions and stamina for 12 seconds and add a match event when issued.

Rough Code
using System;
namespace Socer;

public sealed partial class MatchSimulation
{
    public int CoachOrder;
    public float CoachUntil, CoachReadyAt;
    public string CoachCaption => Seconds<CoachUntil ? CoachOrder==1?"ПРЕССИНГ":CoachOrder==2?"В АТАКУ":"БЕРЕЧЬ СИЛЫ" : "КОМАНДЫ ТРЕНЕРА";
    public bool Command(int order)
    {
        if(!CanFieldCall || order<1 || order>3 || Seconds<CoachReadyAt) return false;
        CoachOrder=order;CoachUntil=Seconds+12;CoachReadyAt=Seconds+25;
        AddEvent("Тренер: "+CoachCaption.ToLowerInvariant());return true;
    }
    Point ApplyCoach(Footballer p,Point target,int possessionTeam,float dt)
    {
        if(p.Keeper || p.Id==Owner || WindupPlayer==p.Id || DuelDefender==p.Id || p.BoxRole>0) return target;
        int dir=Direction(p.Team);
        if(Owner!=p.Id)
        {
            if(p.Instruction==1 && possessionTeam==p.Team)
            {
                // Hold behind the attack; do not abandon the penalty area to
                // stand on the original touchline when defending.
                var local=World(target,p.Team);
                local.X=Math.Min(local.X,Math.Max(p.Home.X,World(Ball,p.Team).X-16));
                target=World(local,p.Team);p.RunUntil=0;
            }
            if(p.Instruction==2 && possessionTeam==p.Team) target.X+=dir*6;
            if(p.Instruction==3) target.Y+=p.Home.Y<34?-6:6;
        }
        if(p.Team==0 && Seconds<CoachUntil)
        {
            if(CoachOrder==1 && possessionTeam!=0 && Owner>=0 && !Players[Owner].Keeper)
            {
                var carrier=Players[Owner];var goal=World(new Point(0,34),0);
                if(p.Id==chasers[0])
                {
                    target=carrier.Position+(goal-carrier.Position).Unit*.85f;
                    p.Action="Тренер · давление на мяч";
                }
                else if(p.Id==secondChasers[0])
                {
                    if(p.AutoMarkId>=0 && Players[p.AutoMarkId].InPlay &&
                        (Players[p.AutoMarkId].Position-goal).Length<28)
                        return target; // Keep the dangerous runner covered during a press.
                    Footballer outlet=null;float best=float.MaxValue;
                    foreach(var q in ActivePlayers)
                    {
                        if(q.Team==0 || q.Keeper || q.Id==Owner)continue;
                        float d=(q.Position-carrier.Position).Length;
                        if(d>3 && d<best && d<19){outlet=q;best=d;}
                    }
                    target=outlet==null?carrier.Position+(goal-carrier.Position).Unit*3:
                        carrier.Position+(outlet.Position-carrier.Position).Unit*Math.Min(4,best*.4f);
                    p.Action="Тренер · перекрывает выход из прессинга";
                }
            }
            if(CoachOrder==2 && possessionTeam==0 && Owner>=0 && !BackLine(p) &&
                p.RunUntil<=Seconds && p.FootballRunUntil<=Seconds)
            {
                var carrier=Players[Owner];int closer=0;
                foreach(var q in ActivePlayers)
                    if(q.Team==0 && q.Id!=Owner && !q.Keeper && !BackLine(q) &&
                        (q.Position-carrier.Position).Length<(p.Position-carrier.Position).Length)closer++;
                if(closer<2 && (p.Position-carrier.Position).Length<27)
                {
                    float side=p.Home.Y<34?-1:1;
                    var option=Clamp(carrier.Position+new Point(dir*(closer==0?-4:9),side*(closer==0?7:10)));
                    if(PassSafety(carrier,p,option)>.45f)
                    {target=option;p.Action=closer==0?"Тренер · поддерживает треугольник":"Тренер · открывается в свободную зону";}
                }
            }
            if(CoachOrder==3) {p.BurstRemaining=0;p.Stamina=Math.Min(1,p.Stamina+dt*.0015f);if(possessionTeam==0)target.X-=dir*3;}
            else p.Stamina=Math.Max(.08f,p.Stamina-dt*.002f);
        }
        return target;
    }
}