DirectOrders.cs

Part of the soccer match simulation that handles the coach's direct orders. Lets the player command pressing, marking, zone guarding and attacking runs, calls field attacks and shots, picks which footballer receives an order, and computes the resulting movement targets and Russian status text.

Rough Code
using System;
using System.Collections.Generic;
using System.Linq;

namespace Socer;

public enum DirectOrderKind { None, PressPlayer, MarkPlayer, GuardZone, AttackRun }

public sealed partial class MatchSimulation
{
    public float FieldAttackUntil, FieldShootUntil;
    readonly HashSet<int> pendingDefensiveOrders=new();
    public Point HomeAttackGoal=>World(new Point(105,34),0);
    public bool CanFieldCall=>!Finished && !MedicalActive && !ShootoutActive && RestartRemaining<=0 && GoalHoldRemaining<=0 && IncidentRemaining<=0 &&
        ShotMissFlightRemaining<=0 && !ManualRestartWaiting;
    public string FieldAttackFailure()
    {
        if(Finished)return "finished";
        if(!CanFieldCall)return "stoppage";
        if(Owner<0 || Players[Owner].Team!=0)return "possession";
        if(WindupPlayer>=0 || KeeperKickPlayer>=0)return "kicking";
        if(Players[Owner].Keeper)return "keeper";
        return "";
    }
    public bool CallAttack(bool desperate)
    {
        if(FieldAttackFailure()!="")return false;
        CancelFieldPass();
        FieldAttackUntil=Seconds+8;
        FieldShootUntil=desperate?Seconds+3:0;
        Players[Owner].PlanUntil=0;Players[Owner].Decision=0;
        return true;
    }
    public int CallMarkOpponent(int targetId)=>CallOpponentOrder(targetId,DirectOrderKind.MarkPlayer);
    public int CallOpponentOrder(int targetId,DirectOrderKind kind,int playerId=-1)
    {
        if(kind is not (DirectOrderKind.MarkPlayer or DirectOrderKind.PressPlayer) || !CanFieldCall ||
            targetId<11 || targetId>=Players.Count || !Players[targetId].InPlay || Players[targetId].Keeper)return -1;
        if(playerId>=0)return GiveDirectOrder(playerId,kind,targetId)?playerId:-1;
        var enemy=Players[targetId];var goal=World(new Point(0,34),0);
        var existing=ActivePlayers.FirstOrDefault(p=>p.Team==0 && p.DirectTargetId==targetId &&
            p.DirectOrder is DirectOrderKind.MarkPlayer or DirectOrderKind.PressPlayer &&
            (p.DirectOrderUntil>Seconds || pendingDefensiveOrders.Contains(p.Id)) &&
            Seconds>=p.FallUntil);
        if(existing!=null)return GiveDirectOrder(existing.Id,kind,targetId)?existing.Id:-1;
        var defender=ActivePlayers.Where(p=>p.Team==0 && !p.Keeper && Seconds>=p.FallUntil &&
            (p.DirectOrder==DirectOrderKind.None || Seconds>p.DirectOrderUntil && !pendingDefensiveOrders.Contains(p.Id)))
            .OrderBy(p=>(p.Position-enemy.Position).Length+
                (p.Id==Owner?16:0)+
                ((p.Position-goal).Length>(enemy.Position-goal).Length?4:0)+
                (p.AutoMarkId>=0 && p.AutoMarkId!=targetId && (Players[p.AutoMarkId].Position-goal).Length<24?18:0))
            .FirstOrDefault();
        if(defender==null)return -1;
        return GiveDirectOrder(defender.Id,kind,targetId)?defender.Id:-1;
    }
    public bool CallMovePlayer(int id,Point target)
    {
        if(!CanFieldCall)return false;
        bool attack=(Owner>=0?Players[Owner].Team:LastTouch)==0;
        if(!GiveDirectOrder(id,attack?DirectOrderKind.AttackRun:DirectOrderKind.GuardZone,point:target))return false;
        Players[id].DirectOrderUntil=Seconds+(attack?12:15);
        return true;
    }
    bool TryFieldShot(Footballer p)
    {
        if(p.Team!=0 || FieldAttackUntil<=Seconds || !CanFieldCall)return false;
        float distance=(HomeAttackGoal-p.Position).Length;
        bool force=FieldShootUntil>Seconds;
        if(!force && !ClearFinishingChance(p))return false;
        if(!BallAtFoot(p) || WindupPlayer>=0)return false;
        if(!PrepareKickFacing(p,HomeAttackGoal))return true;
        p.ShotSetupUntil=0;p.PlanUntil=0;
        p.DecisionReason=force?"Тренер: бить любой ценой":"Тренер: обострить и завершить";
        if(force)BeginShot(p,distance);
        else SetUpShot(p,distance,CarrierPressure(p));
        if(force)ShotTarget.Y+=(Next()-.5f)*(1.5f+Math.Max(0,distance-20)*.12f);
        FieldShootUntil=0;FieldAttackUntil=0;
        return true;
    }
    public bool GiveDirectOrder(int playerId,DirectOrderKind kind,int targetId=-1,Point point=default)
    {
        if(FieldOrderFailure(playerId,kind,targetId,point)!="")return false;
        var p=Players[playerId];
        if(FieldPasser==playerId)CancelFieldPass();
        p.DirectOrder=kind;p.DirectTargetId=kind is DirectOrderKind.PressPlayer or DirectOrderKind.MarkPlayer?targetId:-1;
        p.DirectPoint=point;p.DirectOrderUntil=kind==DirectOrderKind.None?0:Seconds+DirectOrderDuration(kind);
        pendingDefensiveOrders.Remove(playerId);
        if(kind is DirectOrderKind.PressPlayer or DirectOrderKind.MarkPlayer && (Owner>=0?Players[Owner].Team:LastTouch)==0)
            pendingDefensiveOrders.Add(playerId);
        p.PlanUntil=0;p.Decision=0;
        AddEvent($"Тренер · #{p.Number}: {DirectOrderText(p)}");
        return true;
    }
    static float DirectOrderDuration(DirectOrderKind kind)=>kind==DirectOrderKind.AttackRun?12:
        kind==DirectOrderKind.GuardZone?15:kind==DirectOrderKind.PressPlayer?10:18;
    public string FieldOrderFailure(int playerId,DirectOrderKind kind,int targetId=-1,Point point=default)
    {
        if(playerId<0 || playerId>=11 || !Players[playerId].InPlay)return "actor";
        if(kind==DirectOrderKind.None)return "";
        if(Finished)return "finished";
        if(!CanFieldCall)return "stoppage";
        var p=Players[playerId];
        if(p.Keeper)return "actor-keeper";
        if(Seconds<p.FallUntil)return "fallen";
        if(kind is DirectOrderKind.PressPlayer or DirectOrderKind.MarkPlayer)
        {
            if(targetId<11 || targetId>=Players.Count || !Players[targetId].InPlay)return "opponent";
            if(Players[targetId].Keeper)return "target-keeper";
        }
        else if(kind is DirectOrderKind.GuardZone or DirectOrderKind.AttackRun)
        {
            if(kind==DirectOrderKind.AttackRun && (Owner>=0?Players[Owner].Team:LastTouch)!=0)return "run-possession";
            if(!float.IsFinite(point.X) || !float.IsFinite(point.Y) || point.X<0 || point.X>105 || point.Y<0 || point.Y>68)return "point";
        }
        else return "order";
        return "";
    }
    public int PickCommandPlayer(Point target,int enemy=-1)
    {
        var goal=World(new Point(0,34),0);
        var chosen=ActivePlayers.Where(p=>p.Team==0 && !p.Keeper && Seconds>=p.FallUntil)
            .OrderBy(p=>(p.Position-target).Length+
                (p.Id==Owner?16:0)+(p.DirectOrderUntil>Seconds && p.DirectOrder!=DirectOrderKind.None && p.DirectTargetId!=enemy?10:0)+
                (enemy>=11 && (p.Position-goal).Length>(target-goal).Length?4:0))
            .FirstOrDefault();
        return chosen?.Id??-1;
    }

    public bool DefensiveOrderPending(Footballer p)=>pendingDefensiveOrders.Contains(p.Id) &&
        p.DirectOrder is DirectOrderKind.PressPlayer or DirectOrderKind.MarkPlayer;
    public string DirectOrderText(Footballer p)=>p.DirectOrder switch
    {
        DirectOrderKind.PressPlayer when p.DirectTargetId>=0 && DefensiveOrderPending(p)=>$"прессинг #{Players[p.DirectTargetId].Number} при потере мяча",
        DirectOrderKind.MarkPlayer when p.DirectTargetId>=0 && DefensiveOrderPending(p)=>$"опека #{Players[p.DirectTargetId].Number} при потере мяча",
        DirectOrderKind.PressPlayer when p.DirectTargetId>=0=>$"прессинг #{Players[p.DirectTargetId].Number}",
        DirectOrderKind.MarkPlayer when p.DirectTargetId>=0=>$"опека #{Players[p.DirectTargetId].Number}",
        DirectOrderKind.GuardZone=>$"закрывает зону {(int)p.DirectPoint.X}×{(int)p.DirectPoint.Y}",
        DirectOrderKind.AttackRun=>$"рывок в точку {(int)p.DirectPoint.X}×{(int)p.DirectPoint.Y}",
        _=>"свободная игра"
    };

    Point ApplyDirectOrder(Footballer p,Point fallback,int possessionTeam)
    {
        if(p.Team!=0 || p.Keeper || p.DirectOrder==DirectOrderKind.None)return fallback;
        if(p.DirectOrder is DirectOrderKind.PressPlayer or DirectOrderKind.MarkPlayer &&
            (p.DirectTargetId<11 || p.DirectTargetId>=Players.Count || !Players[p.DirectTargetId].InPlay))
        {p.DirectOrder=DirectOrderKind.None;pendingDefensiveOrders.Remove(p.Id);return fallback;}
        if(DefensiveOrderPending(p))
        {
            // An instruction during our attack is for the next defensive phase.
            // Start its normal lifetime when possession is lost, including loose balls.
            p.DirectOrderUntil=Seconds+DirectOrderDuration(p.DirectOrder);
            if(possessionTeam==p.Team)return fallback;
            pendingDefensiveOrders.Remove(p.Id);
        }
        if(Seconds>p.DirectOrderUntil){p.DirectOrder=DirectOrderKind.None;pendingDefensiveOrders.Remove(p.Id);return fallback;}
        if(WindupPlayer==p.Id || DuelDefender==p.Id || Seconds<p.FallUntil)return fallback;
        if(Owner==p.Id)return fallback;
        if(p.DirectOrder is DirectOrderKind.PressPlayer or DirectOrderKind.MarkPlayer)
        {
            int id=p.DirectTargetId;
            if(id<11 || id>=22 || !Players[id].InPlay){p.DirectOrder=DirectOrderKind.None;return fallback;}
            if(possessionTeam==p.Team)return fallback;
            var enemy=Players[id];
            var goal=World(new Point(0,34),p.Team);
            var goalside=(goal-enemy.Position).Unit;
            p.Action=p.DirectOrder==DirectOrderKind.PressPlayer?"Личный прессинг":"Персональная опека";
            return p.DirectOrder==DirectOrderKind.PressPlayer?
                Clamp(enemy.Position+enemy.Velocity*.23f+goalside*.6f):
                Clamp(enemy.Position+enemy.Velocity*.15f+goalside*1.65f);
        }
        if(p.DirectOrder==DirectOrderKind.GuardZone)
        {
            var threat=ActivePlayers.Where(q=>q.Team!=p.Team && !q.Keeper && (q.Position-p.DirectPoint).Length<6)
                .OrderBy(q=>(q.Position-p.DirectPoint).Length).FirstOrDefault();
            p.Action="Закрывает заданную зону";
            if(threat==null)return p.DirectPoint;
            var goal=World(new Point(0,34),p.Team);
            return Clamp(threat.Position+(goal-threat.Position).Unit*1.7f);
        }
        if(p.DirectOrder==DirectOrderKind.AttackRun)
        {
            if(possessionTeam!=p.Team)
            {p.DirectOrder=DirectOrderKind.None;return fallback;}
            if(Owner==p.Id)return fallback; // Carrier steering runs before obstacle avoidance and touch pursuit.
            if(PassReceiver==p.Id && Owner<0)return fallback;
            p.Action=(p.Position-p.DirectPoint).Length<1?"Ждёт пас по команде":"Рывок по команде";
            return OnsideRunTarget(p,p.DirectPoint);
        }
        return fallback;
    }
}