Partial class extending MatchController with the in-match radial command wheel for a football sim. It builds the option list for the clicked target (teammate, opponent or pitch point), validates each order through Match.FieldOrderFailure / FieldPassFailure / FieldAttackFailure, then executes the chosen order (pass, run, press, mark, zone, move, attack, shoot, clear), emits a field ping and posts a short notice. Also closes the wheel when context changes or the match ends.
using System;
using System.Collections.Generic;
using Sandbox;
using Socer;
public enum FieldCommand { Pass, Move, Run, Zone, Press, Mark, Attack, Shoot, Clear }
public readonly record struct FieldCommandOption(FieldCommand Kind,string Glyph,string Name,string Reason);
public sealed partial class MatchController
{
public bool CommandWheelOpen {get;private set;}
public Vector2 CommandWheelAnchor {get;private set;}
public Point CommandWheelPoint {get;private set;}
public int CommandWheelTarget {get;private set;}=-1;
public int CommandWheelActor {get;private set;}=-1;
public bool CommandWheelGoal {get;private set;}
MatchSimulation commandWheelMatch,manualSelectionMatch;
public string CommandNotice {get;private set;}="";
public float CommandNoticeUntil {get;private set;}
static string CommandText(string ru,string en)=>GamePreferences.Russian?ru:en;
string FieldCallContextFailure()=>ClubOpen || StrategyLabOpen || Replay.Playing || MatchIntroActive?
CommandText("Команды доступны во время прямой трансляции матча","Commands are available during the live match"):
Match.Finished?FieldCommandFailureText("finished"):!Match.CanFieldCall?FieldCommandFailureText("stoppage"):"";
string FieldCommandFailureText(string reason)=>reason switch
{
""=>"", "finished"=>CommandText("Матч завершён","The match has finished"),
"stoppage"=>CommandText("Дождитесь возобновления игры","Wait for play to resume"),
"possession"=>CommandText("Нужно владеть мячом","Your team needs possession"),
"offside"=>CommandText("Игрок в офсайде","Player is offside"),
"distance"=>CommandText("Игрок слишком близко или далеко","Player is too close or too far"),
"kicking"=>CommandText("Удар уже начат","A kick is already underway"),
"keeper"=>CommandText("Мяч у вратаря: выберите передачу","Goalkeeper has the ball: choose a pass"),
"receiver"=>CommandText("Выберите другого игрока своей команды для паса","Choose another teammate to receive the pass"),
"actor"=>CommandText("Выберите доступного игрока своей команды","Choose an available teammate"),
"actor-keeper"=>CommandText("Выберите полевого игрока: вратарю этот приказ недоступен","Choose an outfield player: the goalkeeper cannot follow this order"),
"available-actor"=>CommandText("Нет свободного полевого игрока: выберите исполнителя","No free outfield player: choose a player"),
"fallen"=>CommandText("Игрок ещё поднимается после падения","The player is still getting up"),
"opponent"=>CommandText("Выберите действующего игрока соперника","Choose an active opponent"),
"target-keeper"=>CommandText("Нельзя назначить опеку или персональный прессинг вратаря","Cannot assign marking or personal pressing to the goalkeeper"),
"run-possession"=>CommandText("Забегание доступно с мячом у команды","Runs require team possession"),
"point"=>CommandText("Выберите точку внутри поля","Choose a position inside the pitch"),
_=>CommandText("Команда сейчас недоступна","Command unavailable right now")
};
public string CommandWheelTitle=>CommandWheelTarget>=0?$"#{Match.Players[CommandWheelTarget].Number} · {Match.Players[CommandWheelTarget].Profile.Name}":
CommandWheelGoal?CommandText("ВОРОТА СОПЕРНИКА","OPPOSITION GOAL"):CommandText("ТОЧКА ПОЛЯ","PITCH POSITION");
public string CommandWheelActorLabel=>CommandWheelTarget>=0 && CommandWheelTarget<11 && Match.Owner>=0 && Match.Owner!=CommandWheelTarget && Match.Players[Match.Owner].Team==0?
CommandText($"Пас: #{Match.Players[Match.Owner].Number} → #{Match.Players[CommandWheelTarget].Number} · приказ: #{Match.Players[CommandWheelTarget].Number}",
$"Pass: #{Match.Players[Match.Owner].Number} → #{Match.Players[CommandWheelTarget].Number} · order: #{Match.Players[CommandWheelTarget].Number}"):
CommandWheelActor>=0?
CommandText($"Исполнитель: #{Match.Players[CommandWheelActor].Number}",$"Player: #{Match.Players[CommandWheelActor].Number}"):CommandText("Нет доступного исполнителя","No available player");
public void OpenCommandWheel(Point point,int target,Vector2 anchor,bool goal=false)
{
if(ClubOpen || StrategyLabOpen || Replay.Playing || MatchIntroActive || Match.Finished || Match.ManualRestartWaiting)return;
CancelDirectOrder();SetFieldDragSlow(false);
if(target<0 || target>=Match.Players.Count || Match.Players[target].SentOff)target=-1;
CommandWheelPoint=point;CommandWheelAnchor=anchor;CommandWheelTarget=target;CommandWheelGoal=goal;
int selected=manualSelectionMatch==Match && OrderedPlayer>=0 && OrderedPlayer<11 && !Match.Players[OrderedPlayer].SentOff?OrderedPlayer:-1;
CommandWheelActor=target>=0 && target<11?target:selected>=0?selected:Match.PickCommandPlayer(point,target);
commandWheelMatch=Match;CommandWheelOpen=true;accumulator=0;Revision++;
}
public void CloseCommandWheel(){CommandWheelOpen=false;accumulator=0;Revision++;}
void TickCommandWheel()
{
bool unavailable=ClubOpen || StrategyLabOpen || Replay.Playing || MatchIntroActive || Match.Finished || Match.ManualRestartWaiting;
bool changed=commandWheelMatch!=null && commandWheelMatch!=Match;
if(CommandWheelOpen && (changed || unavailable))CloseCommandWheel();
if(changed || unavailable)AwaitingOrder=DirectOrderKind.None;
}
public IReadOnlyList<FieldCommandOption> CommandWheelOptions()
{
FieldCommand[] kinds=CommandWheelTarget>=11?new[]{FieldCommand.Press,FieldCommand.Mark,FieldCommand.Zone,FieldCommand.Move,FieldCommand.Clear}:
CommandWheelTarget>=0?new[]{CommandWheelTarget==Match.Owner?FieldCommand.Shoot:FieldCommand.Pass,FieldCommand.Run,FieldCommand.Zone,FieldCommand.Mark,FieldCommand.Clear}:
CommandWheelGoal?new[]{FieldCommand.Shoot,FieldCommand.Attack,FieldCommand.Move,FieldCommand.Zone,FieldCommand.Clear}:
new[]{FieldCommand.Move,FieldCommand.Run,FieldCommand.Zone,FieldCommand.Attack,FieldCommand.Clear};
var result=new List<FieldCommandOption>(5);
foreach(var kind in kinds)
{
var (glyph,ru,en)=kind switch
{
FieldCommand.Pass=>("➜","Пас сюда","Pass here"),FieldCommand.Move=>("➜","В точку","Move here"),
FieldCommand.Run=>("↗","Забегание","Make a run"),FieldCommand.Zone=>("◇","Держать зону","Hold zone"),
FieldCommand.Press=>("!","Прессинг","Press"),FieldCommand.Mark=>("×","Опека","Mark"),
FieldCommand.Attack=>("↑","В атаку","Attack"),FieldCommand.Shoot=>("⚽","Бить!","Shoot!"),
_=>("↶","Снять приказ","Clear order")
};
result.Add(new(kind,glyph,CommandText(ru,en),CommandFailure(kind)));
}
return result;
}
public string CommandFailure(FieldCommand kind)
{
if(kind==FieldCommand.Clear)return FieldCommandFailureText(Match.FieldOrderFailure(CommandWheelActor,DirectOrderKind.None));
string context=FieldCallContextFailure();
if(context!="")return context;
if(kind==FieldCommand.Pass)return FieldCommandFailureText(Match.FieldPassFailure(CommandWheelTarget));
if(kind is FieldCommand.Attack or FieldCommand.Shoot)
return FieldCommandFailureText(Match.FieldAttackFailure());
var order=kind switch {FieldCommand.Press=>DirectOrderKind.PressPlayer,FieldCommand.Mark=>DirectOrderKind.MarkPlayer,
FieldCommand.Run=>DirectOrderKind.AttackRun,FieldCommand.Move=>(Match.Owner>=0?Match.Players[Match.Owner].Team:Match.LastTouch)==0?
DirectOrderKind.AttackRun:DirectOrderKind.GuardZone,_=>DirectOrderKind.GuardZone};
// Choosing "mark" on our own player arms the next opponent click.
if(kind==FieldCommand.Mark && CommandWheelTarget<11)order=DirectOrderKind.GuardZone;
return FieldCommandFailureText(Match.FieldOrderFailure(CommandWheelActor,order,CommandWheelTarget,CommandWheelPoint));
}
void NotifyCommand(string text){CommandNotice=text;CommandNoticeUntil=Time.Now+3;Revision++;}
public bool ExecuteFieldCommand(FieldCommand kind)
{
if(!CommandWheelOpen || commandWheelMatch!=Match)return false;
string reason=CommandFailure(kind);
if(reason!=""){NotifyCommand(reason);return false;}
int actor=CommandWheelActor,target=CommandWheelTarget;var point=CommandWheelPoint;
bool done=false;FieldPingKind ping=FieldPingKind.Move;int pingPlayer=-1;
if(kind==FieldCommand.Pass)
{done=CallPassTarget(target);if(done)CloseCommandWheel();return done;}
else if(kind is FieldCommand.Attack or FieldCommand.Shoot)
{done=CallAttackTarget(kind==FieldCommand.Shoot);if(done)CloseCommandWheel();return done;}
else if(kind is FieldCommand.Press or FieldCommand.Mark && target>=11)
{done=CallOpponentTarget(target,kind==FieldCommand.Press?DirectOrderKind.PressPlayer:DirectOrderKind.MarkPlayer,actor);if(done)CloseCommandWheel();return done;}
else if(kind==FieldCommand.Clear)
{
done=Match.GiveDirectOrder(actor,DirectOrderKind.None);
if(!done){NotifyCommand(FieldCommandFailureText(Match.FieldOrderFailure(actor,DirectOrderKind.None)));return false;}
point=Match.Players[actor].Position;pingPlayer=actor;
if(actor==Match.FieldPasser || actor==Match.FieldPassReceiver)Match.CancelFieldPass();
if(actor==Match.Owner || CommandWheelGoal)Match.FieldAttackUntil=Match.FieldShootUntil=0;
CancelDirectOrder();ping=FieldPingKind.Clear;
}
else
{
var order=kind switch {FieldCommand.Press=>DirectOrderKind.PressPlayer,FieldCommand.Mark=>DirectOrderKind.MarkPlayer,
FieldCommand.Zone=>DirectOrderKind.GuardZone,FieldCommand.Run=>DirectOrderKind.AttackRun,_=>DirectOrderKind.None};
OrderedPlayer=actor;
if((kind is FieldCommand.Run or FieldCommand.Zone && target>=0 && target<11) || (kind==FieldCommand.Mark && target<11))
{
CloseCommandWheel();AwaitingOrder=order;
NotifyCommand(CommandText($"#{Match.Players[actor].Number}: выберите "+(kind==FieldCommand.Mark?"соперника":"точку поля"),
$"#{Match.Players[actor].Number}: choose "+(kind==FieldCommand.Mark?"an opponent":"a pitch position")));return true;
}
if(kind==FieldCommand.Move){done=CallMoveTarget(actor,point);if(done)CloseCommandWheel();return done;}
else done=Match.GiveDirectOrder(actor,order,target>=11?target:-1,point);
ping=kind switch {FieldCommand.Press=>FieldPingKind.Press,FieldCommand.Mark=>FieldPingKind.Mark,
FieldCommand.Zone=>FieldPingKind.Zone,FieldCommand.Run=>FieldPingKind.Run,
_=>Match.Players[actor].DirectOrder==DirectOrderKind.AttackRun?FieldPingKind.Run:FieldPingKind.Zone};
if(kind is FieldCommand.Press or FieldCommand.Mark)pingPlayer=target;
}
if(!done){NotifyCommand(CommandFailure(kind));return false;}
CloseCommandWheel();EmitFieldPing(ping,point,pingPlayer);
var name=kind switch {FieldCommand.Pass=>CommandText("Пас выбранному игроку","Pass to target"),FieldCommand.Press=>CommandText("Прессинг","Press"),
FieldCommand.Mark=>CommandText("Опека","Mark"),FieldCommand.Zone=>CommandText("Держать зону","Hold zone"),FieldCommand.Run=>CommandText("Забегание","Make a run"),
FieldCommand.Attack=>CommandText("Ищем момент для удара","Look for a shooting chance"),FieldCommand.Shoot=>CommandText("Подготовить удар сейчас","Prepare a shot now"),
FieldCommand.Clear=>CommandText("Приказ снят","Order cleared"),_=>CommandText("Движение в точку","Move to position")};
NotifyCommand((kind is FieldCommand.Pass or FieldCommand.Attack or FieldCommand.Shoot?"":actor>=0?$"#{Match.Players[actor].Number} · ":"")+name);return true;
}
}